# Creative Blockchain — Arc + Circle mega-prompts (Arc Testnet + Arc Mainnet) 996 self-contained Lovable mega-prompts. Source: https://arcprompts.lovable.app ## Network setup [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. ===== SKILL SKILL.md ===== # Lovable · Arc (Testnet + Mainnet) Default to **testnet**; mainnet spends real USDC. Everything here is what actually broke shipping Arc apps on Lovable (four contracts, an agent payout engine, an NFT marketplace with royalties, a 996-prompt hackathon pack). For CCTP bridging and UCP checkout schemas see `dance-ucp-arc-circle`. For the SSR-safe Privy mount see `evvm-privy-integration`. This skill owns the **Lovable × Arc** delta. Full network details: `references/networks.md`. ## Networks (copy verbatim) | Thing | Testnet | Mainnet | | --- | --- | --- | | chainId / CAIP-2 | `5042002` / `eip155:5042002` | `5042` / `eip155:5042` | | RPC | `https://rpc.testnet.arc.network` | `https://rpc.mainnet.arc.io` (Alchemy optional, server-only `ARC_MAINNET_RPC_URL`) | | Explorer (Blockscout) | `https://testnet.arcscan.app` | `https://explorer.arc.io` | | Circle blockchain id | `ARC-TESTNET` | `ARC` | | Circle API key | TEST | LIVE | | Funding | `https://faucet.circle.com/` (~20 USDC/day) | none — real USDC | **RPC hostname trap (testnet).** Docs interleave `rpc.testnet.arc.io` and `rpc.testnet.arc.network`; only `.network` resolves reliably. **Decimals (corrected).** Native USDC balance (`eth_getBalance`, viem `nativeCurrency`) uses **18 decimals**. The USDC ERC-20 interface at `0x3600…0000` uses **6**. EURC 6, cirBTC **8**. Earlier versions of this skill said native = 6 — that was wrong and renders balances off by 10^12. ### Token addresses | Token | Testnet | Mainnet | Decimals | | --- | --- | --- | --- | | USDC | `0x3600000000000000000000000000000000000000` | same | 6 ERC-20 / 18 native | | EURC | `0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a` | `0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1` | 6 | | cirBTC | `0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF` | `0x171A4217b86A807A64eB94757Db6849fb4bDbAA0` | **8** | Viem chains (`src/lib/arc-chain.ts`): ```ts import { defineChain } from "viem"; const native = { name: "USD Coin", symbol: "USDC", decimals: 18 } as const; export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: native, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true }); export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: native, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } } }); ``` ## Testnet/mainnet switch One `NETWORKS` table (chain, tokens, explorer, faucet, Circle id) + server `ARC_NETWORK` env (default `testnet`) + a localStorage `NetworkProvider` in the UI. Key Privy by network (`key={network}`, `supportedChains: [arcTestnet, arcMainnet]`). Gate contracts not yet deployed on mainnet with a "switch back to testnet" card. Keep the Alchemy mainnet URL server-only (proxy via a server fn). Hide the faucet link on mainnet and show a real-money warning. ## Participant setup (2 pasted secrets) Participants paste only `CIRCLE_API_KEY` + `PRIVY_APP_ID`. Lovable runs `scripts/bootstrap-circle.mjs` in the sandbox: generate the 32-byte entity secret → write it to disk with the `wx` flag **before** any network call → RSA-OAEP register via **POST** (not PUT) → create the treasury wallet → print the address to fund. A 409 means the key was auto-primed: register once via the Console configurator. Paste the 64-hex secret, never the recovery blob. SCP deploys need an `idempotencyKey` and `abiJson` as a **stringified** JSON. The whole build must fit in ~5 Lovable credits (single page, minimal Solidity). ## Hard rules (each one cost hours) 1. **Never install `@circle-fin/*` SDKs.** They assume Node and crash the Cloudflare Worker runtime the app deploys to (`[unenv] … not implemented`, `__dirname is not defined`, or a blank 500). Hand-roll `fetch` adapters against `https://api.circle.com/v1/w3s`; the whole client is ~150 lines and uses only Web Crypto. See `references/circle-scp.md`. 2. **`feeLevel: "MEDIUM"`, never a `gasPrice`/fee object.** Circle rejects EIP-1559-style fee payloads on Arc with an opaque 400. 3. **Re-encrypt the entity secret on every request.** Fetch `/config/entity/publicKey`, RSA-OAEP(SHA-256) the 32-byte secret, base64 it. Reused ciphertext is rejected. 4. **`VITE_*` is a banned secret prefix in Lovable.** Store as `PRIVY_APP_ID`, not `VITE_PRIVY_APP_ID`, and surface it through a route loader reading `process.env`. Never ship the Alchemy RPC URL to the browser — proxy it. 5. **Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks** (free tier) and rate-limits wide sweeps. Use the public Arc RPC for log reads, chunk the window, cap the lookback, and cache. Better: read history from the Arcscan v2 REST API. See `references/rpc-and-indexing.md`. 6. **Pin `solc@0.8.24` in devDependencies and use an exact `pragma solidity 0.8.24;`.** A newer local compiler produces bytecode Blockscout cannot reproduce, and verification fails with no useful message. 7. **Wrap Privy in `` + `React.lazy()`.** A static import of `@privy-io/react-auth` crashes workerd. `defaultChain: { id: 5042002, name: "Arc Testnet" } as never` — the Privy type is narrow. 8. **No gas sponsorship needed or available.** USDC *is* gas on Arc; fund the embedded wallet from the faucet and sign normally. No paymaster policy. 9. **Write one JSON file per deployed contract** (`address`, `abi`, `chainId`, `explorer`, `deployTx`) under `src/data/` and have the UI read it. Deploys then go live without a code edit. 10. **Demo-fallback over hard failure.** Every server module must boot with zero secrets and return a realistic envelope flagged `simulated: true`. Never throw at module scope for missing config. 11. **Agent-demo outcomes come from deterministic code, not prompt wording.** Prompts shape the transcript; a post-loop fallback that promotes the best in-policy quote guarantees the close. Never let an LLM own your success rate. See `references/agent-negotiation.md`. 12. **Any price an LLM emits is untrusted input.** Normalise minor-units vs decimals, clamp to `[floor, list]`, and re-derive the charged total from the catalog before it reaches a settlement path. 13. **`modal={false}` on every Radix menu near sticky chrome.** Modal mode locks `` scroll and breaks a `sticky` header the moment the menu opens mid-scroll. See `references/demo-ux.md`. 14. **Every settlement path writes to one shared client ledger.** Add the `record()` call in the same edit as the settlement itself. A judge needs one list — across H2H/H2A/A2A/A2H — proving money moved, with Arcscan links and confirmed/pending status from the Arcscan v2 API. 15. **Keep the cart drawer focused on checkout.** Do not inline transaction history, settlement lists, or mode-switching chrome inside a cart drawer. A compact link to the dedicated settlement surface (`/judge`, `/shop`, or a standalone history page) keeps the checkout path short and the receipt surface authoritative. 16. **Mode-independent editorial/reference pages.** Routes that explain the product (`/markets`, `/moves`, `/primer`, `/deck`, `/judge`) must always render their own content. Never wrap them in a per-mode surface, and hide the mode toggle on them so a persisted GX mode does not silently swap the page in the middle of a demo. 17. **Paginate long settlement/activity lists.** Judge ledgers, receipt panels, and marketplace activity lists should paginate at ~10 items with numbered controls. Long scrollable lists push primary actions below the fold and break demo pacing. 18. **Round hero stats to 2 decimals.** Marketing metrics (e.g., "USDC settled through the rail") should render with 2-decimal precision, not raw contract precision, for readable social proof. 19. **Files over 10 MB never go in the repo.** Large downloads (e.g. a 28 MB `llms-full.txt`) go to Lovable asset storage via `lovable-assets create`, referenced by `.asset.json`; serve `/llms-full.txt` as a redirect route. Generate llms files from the same constants as the prompts. ## Reference index | Read this | For | | --- | --- | | `references/circle-scp.md` | Entity secret onboarding, wallet bootstrap, transfer + contractExecution, polling, error mapping | | `references/rpc-and-indexing.md` | RPC selection, getLogs limits, chunking, caching, Arcscan v2 as indexer | | `references/contracts-and-verify.md` | solc pinning, Blockscout verify recipe, ERC-721 / ERC-2981 / ERC-1271 on Arc | | `references/payments-ux.md` | USDC/EURC/cirBTC toggle, decimals, FX, Privy approval-modal gotchas, treasury panel | | `references/agent-negotiation.md` | Two-agent buyer/seller loops that close: derived budgets, seller floors, final-turn rules, deterministic fallback, quote sanitising | | `references/demo-ux.md` | Judge-facing layer: collapsible run ledgers, cross-mode settlement list, catalog-derived prefills, Radix scroll-lock | | `references/networks.md` | Testnet vs mainnet constants, decimals, the switch pattern | | `references/failure-modes.md` | Full symptom → cause → fix table | ## What worked well (reuse these) - **Circle SCP as the only deploy path.** No EOA, no funded private key in CI, USDC gas from the treasury wallet. Four contracts shipped this way. - **ERC-1271 authorizer contract** so agent/treasury actions are authorized by a contract signature instead of an EOA delegate key. - **ERC-2981 royalties enforced inside the marketplace `buy()`**, atomically, in whichever payment token the buyer selected — a separate royalty leg that can fail independently is a worse demo and a worse product. - **Off-chain session ledger that batches nano-amounts** and settles to Arc at a threshold (~$0.50). A 20 USDC/day faucet then survives a full day of demos; size each unit at ~$0.001. - **Arcscan v2 REST as the indexer** for activity/receipt panels — free, full history, no client-side log scan. - **Client-side UnixFS CIDv1 computation** before pinning, so the user sees the content hash and can verify the pin matched. - **A human-readable error mapper** (`mapChainError`) between chain/Circle errors and UI copy. Retrofitting this late was the single biggest time sink. - **Run ledger = summary bar + collapsed payloads.** State chip, `n/total` steps, one big "View receipt on Arcscan" button, payloads behind a `payload · N fields` disclosure that auto-opens only on failures. - **A single cross-mode settlement list.** H2H, H2A, A2A and A2H all write to one persisted client ledger; judges stop asking whether anything settled. - **Mode-independent editorial pages + hidden mode toggle.** Content routes stayed readable even when the user had a persisted GX mode, and the compact "Recent settlements →" link kept the cart drawer focused on checkout. - **Paginated settlement/activity lists.** 10-item pages with numbered controls kept the judge demo compact and the primary action always visible. ## What I'd do differently next time 1. Build the **indexer read path and the error mapper on day one**. Both were retrofitted after users hit rate limits and opaque 400s. 2. Decide the **contract JSON artifact shape before the first deploy** — redeploys are cheap, but three UI refactors to chase a changed shape are not. 3. Treat **treasury gas as a first-class UI concern** from the start: address, balance, and a `lowGas` warning. Payout failures otherwise look like bugs. 4. Sketch the **multi-token decimal matrix** (6/6/8) once, in one `TOKENS` table, instead of discovering cirBTC's 8 decimals via a wrong balance. 5. Build the **deterministic negotiation fallback before tuning any prompt**. Days went into prompt wording for a two-agent demo that a 15-line best-in-policy promotion fixed outright. 6. Ship the **shared settlement ledger with the first settlement path**, not after a reviewer asks where the transactions are. Retrofitting it meant touching every mode's success handler at once. 7. Decide which routes are **mode-driven vs. mode-independent** before wiring the global toggle. Retrofitting content pages to ignore the saved mode meant touching route wrappers, headers, and mobile drawers at once. 8. Build **pagination into the first settlement/activity panel**, not after the list grows long. A 10-item default with numbered controls keeps demos from turning into scrolling exercises. ## Success checkpoints 1. `bootstrap-circle.mjs` prints treasury wallet id + address, and the raw entity-secret hex is on disk **before** any network call. 2. Treasury funded at `faucet.circle.com` → Arc Testnet. 3. `deploy-arc.mjs` polls to `status: "COMPLETE"` and writes `src/data/.json` with `address` + `abi` + `chainId: 5042002`. 4. `verify-arc.mjs` returns 200; a minute later `is_verified: true` on `https://testnet.arcscan.app/api/v2/smart-contracts/{address}`. 5. Google login via Privy provisions an embedded wallet on 5042002 and a real tx confirms on Arcscan under that address. ## Out of scope CCTP bridging (`dance-ucp-arc-circle`). UCP checkout schemas (same skill). Hardhat + funded-EOA deploys — Arc's pitch is "no EOA, USDC gas from a Circle wallet"; use SCP. ===== SKILL references/agent-negotiation.md ===== # Agent-to-agent negotiation that actually closes An LLM buyer agent and an LLM seller agent negotiating over a catalog, then settling on Arc. The settlement leg is the easy part. The demo fails in the *negotiation* leg, and it fails silently: a polite transcript that ends with "unfortunately that's outside my budget" and no quote to pay. Target: ~90% of runs end in a deal, and the ~10% that don't say why. ## 1. Never hardcode the budget The single biggest cause of a dead demo. A goal string like `"buy a snapback for under 0.03 USDC"` against a catalog whose cheapest snapback is `0.047` can only ever produce a refusal — the model is behaving correctly. Derive the budget from the live catalog at request time: ```ts function deriveBudget(catalog: Item[], category?: string) { const pool = category ? catalog.filter(i => i.category === category) : catalog; const cheapest = Math.min(...pool.map(i => i.priceUsd)); // headroom so the buyer has room to concede, capped by the spend policy return Math.min(cheapest * 1.35, POLICY.maxTicketUsd); } ``` Render the derived number into the buyer prompt. Never let a human-typed goal string be the only source of a numeric constraint — parse a user-supplied number if present, but clamp it up to at least the derived floor and tell the user you did. ## 2. Give the seller a floor, and forbid flat refusals A seller prompted only with a list price has exactly one move. Put both numbers in the prompt and name the allowed moves: - `listUsd` and `floorUsd` (e.g. 15% below list) rendered explicitly. - "You may discount down to the floor. You may substitute a cheaper item in the same category. You may not refuse outright — always counter." - Never expose the floor as a negotiating position in turn one; discount in steps so the transcript reads like a negotiation. ## 3. Name the final turn Both agents get "this is turn N of M; on the final turn you must accept the best offer on the table if it is within policy." Models will happily haggle until the loop runs out otherwise. Keep M small (4–5); more turns lower the close rate because there's more room to wander. ## 4. Deterministic close — prompts never own the success rate Prompt tuning gets you to maybe 70%. The last 20 points come from code that runs *after* the loop: ```ts // track every quote the seller ever put on the table const inPolicy = quotes.filter(q => q.totalUsd <= budget && q.totalUsd >= floorFor(q.sku)); if (!outcome.accepted && inPolicy.length) { const best = inPolicy.sort((a, b) => a.totalUsd - b.totalUsd)[0]; outcome = { accepted: true, finalQuote: best, closedBy: "fallback" }; transcript.push(syntheticAcceptTurn(best)); // so the UI reads coherently } ``` Mark the fallback (`closedBy: "fallback" | "agent"`) rather than hiding it — it's honest, and it's a talking point about deterministic guard-rails around non-deterministic agents. ## 5. Treat every LLM-emitted price as untrusted input Before a quote reaches a settlement path: - **Normalise units.** Models flip between decimals and minor units (`0.015` vs `15000`). If the value is >1000× the catalog price, divide by 1e6 rather than trusting it. - **Clamp** to `[floorUsd, listUsd]` for the quoted SKU. - **Re-derive** the total from the catalog price × quantity; use the model's number only to pick the SKU and the discount, never as the amount charged. - **Reject unknown SKUs.** Match against the catalog, don't accept invented ids. The chain leg then only ever sees a number your own code produced. ## 6. Model the no-deal explicitly The remaining failures should be *legible*, not a transcript that trails off. Return a structured outcome: ```ts type Outcome = | { accepted: true; finalQuote: Quote; closedBy: "agent" | "fallback" } | { accepted: false; reason: string }; // e.g. "No in-policy quote under $0.04 after 5 turns" ``` Render `reason` in the UI next to the transcript. A guard-rail that visibly fires is a feature; a silent stall is a bug. ## 7. Testing the close rate Run the loop 20× headless against the real catalog and count `accepted`. Do this after any prompt edit — prompt changes regress the close rate in ways that a single manual run will not surface. If the rate drops, add code, not adjectives. ===== SKILL references/circle-scp.md ===== # Circle SCP + developer-controlled wallets on Arc Everything below runs on plain `fetch` + Web Crypto inside a Cloudflare Worker. Do **not** install `@circle-fin/developer-controlled-wallets` or `@circle-fin/smart-contract-platform` — they pull Node-only deps and break the Worker build (blank 500, `[unenv] … is not implemented yet!`, `__dirname is not defined`). This was tried, reverted, and is the single most expensive mistake available on this stack. Base URL: `https://api.circle.com/v1/w3s` · blockchain id: `ARC-TESTNET`. ## Entity secret — the write-once traps The entity secret is the root of every developer-controlled wallet operation. It is **write-once per Circle account** and Circle never reveals it back. 1. **Persist the raw 32-byte hex to disk BEFORE any network call.** Codify it: ```js const raw = crypto.randomBytes(32).toString("hex"); fs.writeFileSync("circle-entity-recovery.json", JSON.stringify({ raw }, null, 2), { flag: "wx" }); // ONLY THEN encrypt + register ``` 2. **Registration is `POST /v1/w3s/config/entity/entitySecret`**, not `PUT` (the doc generator sometimes shows `PUT`; it 404s). 3. **Expect `409 already registered` even on a brand-new account.** Circle's onboarding pre-primes the entity. Do not chase this with more new accounts — go through Console → Configurator and paste your own base64 ciphertext. 4. **The recovery blob Circle returns is not the entity secret.** Pasting it into `CIRCLE_ENTITY_SECRET` yields `invalid entity secret`. 5. A new API key on the same account inherits the same registered secret. ## Per-request ciphertext (required) Circle rejects reused ciphertext, so encrypt fresh on every call: ```ts async function entitySecretCiphertext(): Promise { const res = await fetch(`${API}/config/entity/publicKey`, { headers: headers() }); const pem = (await res.json()).data.publicKey; const key = await crypto.subtle.importKey( "spki", pemToDer(pem), { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"], ); const cipher = await crypto.subtle.encrypt( { name: "RSA-OAEP" }, key, hexToBytes(process.env.CIRCLE_ENTITY_SECRET!), ); return btoa(String.fromCharCode(...new Uint8Array(cipher))); } ``` `crypto.subtle` + `atob`/`btoa` are available in workerd; `node:crypto`'s RSA path is not reliable there. ## Shared POST envelope Every developer transaction needs all four of these or you get an opaque 400: ```ts { idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: await entitySecretCiphertext(), walletId: process.env.CIRCLE_TREASURY_WALLET_ID, feeLevel: "MEDIUM", // string. NEVER a { gasPrice } / EIP-1559 object ...body, } ``` `feeLevel: "MEDIUM"` is the fix for the `gasPrice` validation error on Arc. ### Transfer `POST /developer/transactions/transfer` ```ts { destinationAddress, amounts: ["0.001"], blockchain: "ARC-TESTNET", tokenAddress /* omit for native USDC gas token */ } ``` Amounts are **decimal strings**, not atomic units — the opposite convention from viem, and an easy 1e6 error. ### Contract call `POST /developer/transactions/contractExecution` ```ts { contractAddress, abiFunctionSignature: "log(address,uint256,string)", abiParameters: [...] } ``` ### Deploy (SCP) `POST /contracts/deploy` — the body must include `name`, `walletId`, `blockchain: "ARC-TESTNET"`, `abiJson: JSON.stringify(abi)` (**string**, not the array), `bytecode: "0x…"`, `constructorParameters: []`, `feeLevel`, `idempotencyKey`, `entitySecretCiphertext`. Then poll `GET /contracts/{contractId}` until `data.contract.status === "COMPLETE"`; the address is at `data.contract.contractAddress`. ## Polling transactions `GET /transactions/{id}` every ~2.5s up to ~90s. Return once `txHash` exists and state is `SENT` / `CONFIRMED` / `COMPLETE`. Throw on `FAILED`, `CANCELLED`, `DENIED`, surfacing `transaction.errorReason` — that field is the only signal you get for gas exhaustion. ## Error mapping Raw Circle strings are unusable in a UI. Map them, and always include the short raw text as a collapsible `detail`: | Raw signal | User-facing copy | | --- | --- | | `insufficient funds` / `gas required exceeds` | "Not enough USDC for gas on Arc. Top up at faucet.circle.com." | | `circle_tx_failed:*` with an errorReason | The reason, plus treasury address + balance | | `missing_secret:CIRCLE_*` | Fall back to demo mode, flag `simulated: true` | | timeout | "Still pending on Arc — check the treasury on Arcscan." | ## Secrets `CIRCLE_API_KEY`, `CIRCLE_ENTITY_SECRET` (64-hex), `CIRCLE_TREASURY_WALLET_ID`, `CIRCLE_TREASURY_ADDRESS`. All server-only, read inside handlers — never at module scope, and never with a `VITE_` prefix. ===== SKILL references/contracts-and-verify.md ===== # Contracts, verification, and the standards that pay off on Arc ## Compiler pinning (verification depends on it) - Pin `solc@0.8.24` in **devDependencies**, not just referenced by a script. - Contract pragma: exact `pragma solidity 0.8.24;` — no caret. - `optimizer: { enabled: true, runs: 200 }` on both compile and verify. A newer local solc (e.g. 0.8.36) yields bytecode Arcscan cannot reproduce and verification fails with "Unable to verify" and no diagnostic. ## Blockscout verification (no API key, no plugin) ```js const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { "MoveMarket.sol": { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: "MoveMarket.sol:MoveMarket", compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); await fetch("https://testnet.arcscan.app/api?" + params, { method: "POST" }); ``` `Smart-contract verification started` (200) means **queued**, not verified. Poll `GET /api/v2/smart-contracts/{address}` for `is_verified: true`; first submission can take a couple of minutes. If the contract takes constructor args, pass the ABI-encoded args explicitly rather than relying on autodetect — autodetect fails silently on address args. ## Standards that earned their keep ### ERC-2981 royalties, settled inside `buy()` Compute the royalty from `royaltyInfo(tokenId, price)` and pay both legs in the buyer's selected ERC-20 **inside the same transaction**: ``` buy(tokenId): (receiver, royaltyAmount) = royaltyInfo(tokenId, price) transferFrom(buyer, receiver, royaltyAmount) // revert "royalty_failed" transferFrom(buyer, seller, price - royaltyAmount) // revert "pay_failed" safeTransferFrom(seller, buyer, tokenId) ``` Distinct revert strings matter — they are what lets the UI say "the creator royalty leg failed" instead of "transaction reverted". Show the buyer the price split (creator vs seller) before they confirm. ### ERC-1271 for agent/treasury authorization A Circle developer-controlled wallet has no EOA private key you can delegate. Deploy a small authorizer contract that implements `isValidSignature(bytes32,bytes) → 0x1626ba7e` and keeps an on-chain allowlist of agent addresses. Gateway-style actions then verify against the contract, and authorization can be granted/revoked on-chain without ever exporting a key. ### ERC-721 with off-chain metadata `tokenURI` points at `ipfs://`. Compute the UnixFS CIDv1 in the browser before pinning so the user sees the hash, and warn clearly when the local hash cannot be matched pre-pin (chunking parameters differ) rather than silently showing a mismatch. ## Deployment artifacts One JSON per contract in `src/data/`: ```json { "address": "0x…", "deployTx": "0x…", "abi": [ … ], "chainId": 5042002, "network": "Arc Testnet", "explorer": "https://testnet.arcscan.app" } ``` The UI imports these directly, so a redeploy is a data change, not a code change. Agree the shape before the first deploy — changing it later means touching every panel. ===== SKILL references/demo-ux.md ===== # Demo UX — the judge-facing layer An Arc demo is judged on whether a reviewer can *see* that something settled. These three patterns did more for perceived quality than any contract work. ## 1. Collapse the JSON, surface the receipt Agent run ledgers (H2A/A2A step lists with request/response payloads) render as a wall of code on mobile. Restructure the ledger, don't shrink the font. Summary bar above the steps: - state chip — `running` / `settled` / `failed` - progress — `5/7 steps`, with a thin progress track - **primary action**: `View receipt on Arcscan` for the terminal settlement tx, plus a copy-hash button. This is the single element judges look for. - a master **Raw JSON** switch that force-expands every payload at once Per step: - prose-first title in bold (`Human confirmed spend`); technical titles (`POST /api/public/purchase`) get mono, smaller, muted - payload collapsed by default behind a disclosure that advertises its size: `payload · 6 fields` / `· 24 lines` - **auto-expand only when the step failed or was blocked** — a failure should never require a click to diagnose - keying the payload component on the master-toggle value is the cheap way to make "Raw JSON" re-drive per-step local state Linkify tx hashes inside payload text to `https://testnet.arcscan.app/tx/` so the raw view is also navigable. ## 2. One settlement ledger across every payment mode Every mode (H2H cart, H2A agent run, A2A negotiation, A2H payout/claim) writes to a single client-side ledger. Judges get one list that proves the demo moved real money. Shape: ```ts type TxRecord = { id: string; // tx hash when known, else a uuid hash?: string; mode: "h2h" | "h2a" | "a2a" | "a2h"; label: string; // "Snapback cap ×1", "Move #12 royalty" amount: string; // human decimals, already token-scaled token: "USDC" | "EURC" | "cirBTC"; at: number; // epoch ms }; ``` - persist to `localStorage`, newest first, cap at ~50 entries - one `record()` helper imported by every settlement path; add the call in the same edit that adds the settlement, or it will never be added - render relative time (`2m ago`), a mode chip, amount + token, an Arcscan link, copy-hash, and a **confirmed / pending** status - resolve status from the Arcscan v2 REST API through a server function, with a manual refresh — do not poll the RPC from the browser - surface the panel in at least two places: the judge route (all modes) and the cart/shop (H2H only, filtered) ## 3. Prefill demo inputs from live data, never hardcode amounts Any demo input containing a number ("Buy a snapback under 0.03 USDC") rots the moment catalog prices change, and the failure looks like a broken agent. - compute the suggestion from the live catalog in a **client-safe shared module** so the server negotiator and the UI agree on one `deriveBudget` - keep re-prefilling the field until the user edits it — track a `dirty` ref, not just "field is empty" - show the derived number as a hint (`Recommended budget 0.052 USDC`) and a **Reset to recommended** link once the text diverges ## 4. Sticky chrome vs Radix menus Radix `DropdownMenu`/`Select` default to `modal`, which sets `overflow: hidden` and `position: relative` on `` while open. That kills the containing block for a `sticky` header — the header disappears mid-scroll the instant the menu opens. Pass `modal={false}` on every menu that is *not* a real overlay (nav "More", currency toggle, filters). Behaviour and keyboard a11y are unchanged; the body scroll lock goes away. Verify with Playwright: scroll ~1200px, open the menu, assert the header's bounding box is still `y == 0`. ## 5. Cart drawer = checkout only A cart drawer is not a dashboard. Inlining a "Recent settlements" panel pushes line items, totals, and the primary pay button below the fold and duplicates a surface that already exists elsewhere. - Keep the cart footer to: total, live token equivalent, pay button, optional Shopify checkout, and a single compact link to the settlement history page. - Use a `Link` from the router (not an external ``) so the drawer closes and the user lands on the authoritative history surface. - Render the full settlement list on a dedicated route (`/judge` is the StreetRail convention) or on the shop page, never inside the drawer. ===== SKILL references/failure-modes.md ===== # Arc on Lovable — failure modes | Symptom | Cause | Fix | |---|---|---| | App returns a blank 500 after adding Circle features | `@circle-fin/*` SDK pulled Node-only deps into the Worker | Remove the SDK; hand-roll `fetch` + Web Crypto against `api.circle.com/v1/w3s` | | `[unenv] X is not implemented yet!` / `__dirname is not defined` | Node-only package in the Worker bundle | Replace with a Worker-safe library or a plain HTTP call | | `add_secret` rejects `VITE_PRIVY_APP_ID` | `VITE_*` is a reserved prefix in Lovable | Save as `PRIVY_APP_ID`, expose via a route loader reading `process.env` | | DNS failure on `rpc.testnet.arc.io` | Wrong hostname in Circle/Arc docs | Use `rpc.testnet.arc.network` | | Circle 409 on a brand-new API key | Onboarding pre-primed the entity secret | Register through Console → Configurator with your own base64 ciphertext | | `invalid entity secret` after pasting | Pasted the recovery blob, not the 64-hex secret | Use the raw hex you persisted before registration | | SCP deploy 400 | Missing `idempotencyKey`, or `abiJson` sent as an array | Send a UUID + `JSON.stringify(abi)` | | Circle tx 400 mentioning `gasPrice` | Sent an EIP-1559/fee object | `feeLevel: "MEDIUM"` (string) | | Circle rejects the ciphertext | Reused a previous ciphertext | Re-encrypt per request against a freshly fetched public key | | Deploy hangs forever | Polling the wrong field | Wait for `data.contract.status === "COMPLETE"` | | `eth_getLogs` errors after ~10 blocks | Alchemy free-tier Arc cap | Use the public Arc RPC for logs; chunk + shrink the window on range errors | | RPC rate-limit errors in a payout/activity panel | Unbounded lookback and polling remounts | Cap lookback (~5,000 blocks), cache results ~60s, prefer Arcscan v2 | | Activity panel misses old events | RPC window can't reach the first mint | Read history from the Arcscan v2 REST API, keep RPC as fallback | | Arcscan "Unable to verify" | Local solc newer than the deployed pragma | Pin `solc@0.8.24` in devDependencies; exact pragma; matching optimizer runs | | Verification "started" but never verified | Queued, not finished | Poll `/api/v2/smart-contracts/{address}` for `is_verified: true` | | Native balance off by 10^12 | Native USDC read with 6 decimals | Native = 18 decimals; only the ERC-20 interface is 6 | | Tx rejected / wrong chain on mainnet | chainId mismatch (5042 vs 5042002) | Read chain from the NETWORKS table for the active toggle | | Circle 401/404 on mainnet | TEST key used with blockchain `ARC` | LIVE key + `ARC` blockchain id | | Commit fails: file exceeds 10 MB | Large download written into the repo | Upload with `lovable-assets create`, commit the `.asset.json` pointer | | cirBTC balance off by 100× | Hardcoded 1e6 conversion | Read decimals from the `TOKENS` table (cirBTC is 8) | | Privy modal shows USDC while approving EURC/cirBTC | Modal always shows the gas token | Render your own pre-confirmation summary — not a bug | | Approve step silently skipped | Token address env unset → zero address | Gate zero-address tokens in the switcher | | Privy modal asks for ETH | Arc gas is USDC | Fund the wallet at `faucet.circle.com` → Arc Testnet | | Privy crashes SSR / workerd | Static import of `@privy-io/react-auth` | `` + `React.lazy()` | | Payout fails with an opaque error | Treasury out of USDC gas | Surface treasury address + balance + `lowGas` warning; refill at the faucet | | Faucet drained mid-demo | Per-payout amounts too large | ~$0.001 units, off-chain batching, settle at ~$0.50 | | CoinGecko 401 | Demo key sent to the pro host | Demo host + `x-cg-demo-api-key` header; keep a static fallback rate | | "Claim"/action looks on-chain but isn't | UI-only state | Anchor it with a real contract call and show the Arcscan link | | Mode/tab switch does nothing on the published site | Mode mapping duplicated across routes | Centralise mode → surface mapping in one component | | Agent negotiation always ends in a refusal | Budget hardcoded in the goal string, below every catalog price | Derive the budget from the live catalog at request time (cheapest in-policy item + headroom) | | Seller agent refuses instead of countering | Prompt only gave it a list price | Render `listUsd` + `floorUsd` and forbid flat refusals — discount or substitute | | Quote total off by ~1e6 | LLM returned minor units, not decimals | Normalise + clamp in code; re-derive the charged total from the catalog | | Run ends with no quote and no explanation | No-deal outcome not modelled | Return `{ accepted: false, reason }` and render the reason next to the transcript | | Sticky header vanishes when a dropdown opens mid-scroll | Radix menus default to `modal`, setting `overflow:hidden` + `position:relative` on ``, which breaks the sticky containing block | `modal={false}` on non-overlay menus (nav "More", currency toggle) | | Prefilled agent goal fails against live prices | Budget hardcoded in the goal string | Derive it from the catalog; dirty-flag the input so edits stick | | Run ledger reads as a wall of code | Every payload rendered expanded | Collapsible payloads + a summary bar with the receipt button | | Judges ask "did anything actually settle?" | No cross-mode transaction list | One shared client ledger written by every settlement path | | Cart drawer feels crowded / pushes pay button below fold | Inline transaction history or settlement list inside the cart | Move history to a dedicated route; keep the cart footer to totals, pay button, and a compact history link | ===== SKILL references/networks.md ===== # Arc networks reference Testnet: chainId 5042002, RPC https://rpc.testnet.arc.network, explorer https://testnet.arcscan.app, Circle id ARC-TESTNET, faucet https://faucet.circle.com/ (~20 USDC/day), TEST API key. Mainnet: chainId 5042, RPC https://rpc.mainnet.arc.io (Alchemy optional, server-only ARC_MAINNET_RPC_URL), explorer https://explorer.arc.io, Circle id ARC, no faucet, LIVE API key, real money. Decimals: native USDC 18; ERC-20 USDC 6; EURC 6; cirBTC 8. Tokens mainnet: EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0, USDC 0x3600000000000000000000000000000000000000. Toggle: ARC_NETWORK env (default testnet); UI NetworkProvider in localStorage "arc-network"; Privy key={network}; gate undeployed contracts on mainnet. Sources: https://docs.arc.io/llms-full.txt , https://developers.circle.com/llms-full.txt ===== SKILL references/payments-ux.md ===== # Multi-stablecoin payment UX on Arc ## One TOKENS table per network, three decimal widths ```ts export const TOKEN_DECIMALS = { USDC: 6, EURC: 6, cirBTC: 8 } as const; // native USDC balance = 18 export const TOKENS = { testnet: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF", }, mainnet: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0", }, } as const; ``` cirBTC is **8 decimals**. Every `toAtomic`/`fromAtomic` must read from this table — a hardcoded `1e6` shows a cirBTC balance off by 100×. USDC is both the native gas token and an ERC-20 at the same address. For a Circle transfer, **omit** `tokenAddress` to move native USDC; include it for EURC/cirBTC. ## Global currency toggle A single context (`PayTokenContext`) driving every payment surface beats a per-panel selector: users pick once, and every quote, balance and receipt re-denominates. Persist the choice. Show the **normal** amount, not micro-units: `0.001 USDC`, never `1000 µUSDC`. Display USD equivalents next to non-USD tokens. ## FX Live rates via Frankfurter (EUR) + CoinGecko (BTC), cached server-side for ~5 minutes. CoinGecko demo keys go to the demo host with the `x-cg-demo-api-key` header — sending a demo key to the pro host 401s. Always keep a static fallback rate so the UI degrades instead of blanking. ## Privy approval-modal gotchas (users will report these as bugs) - The modal always shows the **gas-token** balance (USDC on Arc), whatever ERC-20 the tx targets. - The "Approval amount" row renders **blank** for tokens Privy has no metadata for — i.e. EURC and cirBTC on Arc. Compensate in your own UI: 1. Render a pre-confirmation summary above the confirm button: "You'll approve `{amount} {symbol}` to be spent by `{contractName}`. Token: `0xABCD…1234`" — matching the short address Privy shows, so users can cross-check. 2. If a token address is unset (zero address), **gate it in the switcher** (disabled + hint). Never let the form silently skip the `approve` step. ## Transfer / buy preflight Before sending, check on-chain and fail with copy the user can act on: ownership (`ownerOf`), approval (`getApproved`/`isApprovedForAll`), ERC-20 allowance and balance for the selected token, and recipient validity. A `mapChainError` helper turns `user rejected`, `insufficient funds`, `ERC721IncorrectOwner`, `royalty_failed`, `pay_failed` into one-line copy plus a collapsible raw detail. ## Treasury visibility Agent-driven payouts fail opaquely when the Circle treasury runs out of gas. Surface treasury address, USDC/EURC/cirBTC balances, and an explicit `lowGas` warning with a faucet link. Same for the user's embedded wallet. ## Faucet economics The Circle faucet drips ~20 USDC/day for Arc Testnet. Size demo flows around it: per-payout units of ~$0.001, an off-chain session ledger that accrues and settles on-chain only when the open batch crosses ~$0.50, and per-payout / daily caps enforced in code. One demo day then costs cents. ===== SKILL references/rpc-and-indexing.md ===== # RPC selection and reading history on Arc ## Two endpoints, two jobs Arc's own public RPC and a paid provider endpoint (Alchemy) have opposite strengths. Split them: ```ts const rpcUrl = () => process.env.ARC_RPC_URL || "https://rpc.testnet.arc.network"; // writes / calls const logsRpcUrl = () => process.env.ARC_LOGS_RPC_URL || "https://rpc.testnet.arc.network"; // eth_getLogs ``` **Alchemy's free Arc endpoint caps `eth_getLogs` at a 10-block range.** Point it at reads/calls only. The public Arc RPC has no such cap but rate-limits wide sweeps, so still bound and cache them. **Never expose either URL to the browser.** `VITE_*` is a banned secret prefix in Lovable and an Alchemy key in the client is a leak — proxy every RPC call through a server function or a `src/routes/api/` handler. ## Bounded, chunked, cached log reads When you must read logs from RPC: - Cap total lookback at ~5,000 blocks from head (going further is what triggers rate limiting, and the collection's first mint is far older anyway). - Chunk into small windows and detect range rejections by message text — providers phrase them differently: `block range`, `range too large`, `range should work`, `too many`. On a hit, halve the window and retry. - Cache results server-side for ~60s. Payout inboxes and activity panels poll, and without the cache each mount replays the whole sweep. ## Prefer Arcscan v2 as the indexer Arcscan is Blockscout, so it exposes a free REST v2 API with full history — strictly better than an RPC sweep for any "show me everything that happened" panel. Use it as the primary source and keep the chunked RPC sweep only as a fallback when the explorer is unreachable. ``` GET https://testnet.arcscan.app/api/v2/addresses/{address}/logs GET https://testnet.arcscan.app/api/v2/smart-contracts/{address} GET https://testnet.arcscan.app/api/v2/transactions/{hash} ``` Decode the returned topics/data with viem's `parseAbiItem` + `decodeEventLog` so both paths produce the same shape. No API key required. Pattern that worked: one server module owns "market activity", joins `Listed` / `Cancelled` / `Sold` / `RoyaltyPaid` / ERC-721 `Transfer` into a single typed timeline, and the UI does search/filter/sort on top of that already-joined list. Doing the join client-side across four separate log queries is slow and rate-limits. ## Linking out Every hash and address in the UI should link to `https://testnet.arcscan.app/tx/{hash}` / `/address/{addr}`. Judges and users treat an unlinked hash as unverifiable — linkify hashes even inside rendered JSON receipt blocks. ===== ARC_CHAIN_SNIPPET ===== // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ===== TOKEN_REGISTRY_SNIPPET ===== // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ===== ENTITY_SECRET_RULES ===== ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. ===== CIRCLE_RULES ===== CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. ===== NETWORK_SWITCH ===== NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. ===== GAS_NOTE ===== GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. ===== TROUBLESHOOTING ===== TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. ===== IDEA dance-ai-dance-dataset-repository-11 · dance · AI Dance Dataset Repository ===== Pitch: Pin curated datasets of labeled dance images and JSON metadata on IPFS for AI training. Page: https://arcprompts.lovable.app/ideas/dance-ai-dance-dataset-repository-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AI Dance Dataset Repository" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin curated datasets of labeled dance images and JSON metadata on IPFS for AI training. Discipline: Dance & Choreography (machine learning data). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS guarantees permanent, verifiable storage of valuable AI training data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AIDanceDatasetRepository.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAIDanceDatasetRepository /// @notice Pin curated datasets of labeled dance images and JSON metadata on IPFS for AI training. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAIDanceDatasetRepository { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AIDanceDatasetRepository` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AIDanceDatasetRepository.sol AIDanceDatasetRepository` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a machine learning data, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-beatsync-provenance-2 · dance · BeatSync Provenance ===== Pitch: Authenticate unique dance beat patterns as NFTs for royalty-tracked reuse in choreography. Page: https://arcprompts.lovable.app/ideas/dance-beatsync-provenance-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "BeatSync Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate unique dance beat patterns as NFTs for royalty-tracked reuse in choreography. Discipline: Dance & Choreography (rhythmic innovation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens link beats to creators with transparent ownership history. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/BeatSyncProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title BeatSyncProvenance /// @notice ERC-721 provenance for: Authenticate unique dance beat patterns as NFTs for royalty-tracked reuse in choreography. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract BeatSyncProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("BeatSyncProvenance", "BEATSY") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs BeatSyncProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/BeatSyncProvenance.sol BeatSyncProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a rhythmic innovation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-choreo-collaboration-hub-5 · dance · Choreo Collaboration Hub ===== Pitch: Facilitate collaborative dance projects with blockchain-secured contribution records, gas-free. Page: https://arcprompts.lovable.app/ideas/dance-choreo-collaboration-hub-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Choreo Collaboration Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Facilitate collaborative dance projects with blockchain-secured contribution records, gas-free. Discipline: Dance & Choreography (joint choreography creation). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet and sponsored tx enable seamless multi-user editing without crypto hurdles. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreoCollaborationHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogChoreoCollaborationHub /// @notice Facilitate collaborative dance projects with blockchain-secured contribution records, gas-free. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogChoreoCollaborationHub { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreoCollaborationHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreoCollaborationHub.sol ChoreoCollaborationHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every joint choreography creation action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-choreo-ledger-0 · dance · Choreo Ledger ===== Pitch: Securely prove original choreography ownership and timestamp creations on a public ledger. Page: https://arcprompts.lovable.app/ideas/dance-choreo-ledger-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Choreo Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely prove original choreography ownership and timestamp creations on a public ledger. Discipline: Dance & Choreography (movement attribution). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts provide immutable proof of choreography authorship, accessible globally and transparently. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreoLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ChoreoLedger /// @notice Securely prove original choreography ownership and timestamp creations on a public ledger. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ChoreoLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreoLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreoLedger.sol ChoreoLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a movement attribution action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-choreo-licensing-hub-9 · dance · Choreo Licensing Hub ===== Pitch: Create a decentralized marketplace for buying and selling choreography licenses directly onchain. Page: https://arcprompts.lovable.app/ideas/dance-choreo-licensing-hub-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Choreo Licensing Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create a decentralized marketplace for buying and selling choreography licenses directly onchain. Discipline: Dance & Choreography (rights marketplace). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts automate license agreements and transparent payments without middlemen. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreoLicensingHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ChoreoLicensingHub /// @notice Create a decentralized marketplace for buying and selling choreography licenses directly onchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ChoreoLicensingHub { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreoLicensingHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreoLicensingHub.sol ChoreoLicensingHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a rights marketplace action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-choreo-nft-vault-1 · dance · Choreo NFT Vault ===== Pitch: Securely store and share choreography as NFTs without users handling gas payments. Page: https://arcprompts.lovable.app/ideas/dance-choreo-nft-vault-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Choreo NFT Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely store and share choreography as NFTs without users handling gas payments. Discipline: Dance & Choreography (digital choreography ownership). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet with Google sign-in and sponsored tx eases NFT minting experience. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreoNFTVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogChoreoNFTVault /// @notice Securely store and share choreography as NFTs without users handling gas payments. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogChoreoNFTVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreoNFTVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreoNFTVault.sol ChoreoNFTVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every digital choreography ownership action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-choreo-royalty-pool-3 · dance · Choreo Royalty Pool ===== Pitch: Distribute royalties automatically among choreographers, dancers, and producers using smart contracts. Page: https://arcprompts.lovable.app/ideas/dance-choreo-royalty-pool-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Choreo Royalty Pool" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Distribute royalties automatically among choreographers, dancers, and producers using smart contracts. Discipline: Dance & Choreography (rights management). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts transparently split and enforce royalty terms without intermediaries. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreoRoyaltyPool.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ChoreoRoyaltyPool /// @notice Distribute royalties automatically among choreographers, dancers, and producers using smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ChoreoRoyaltyPool { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreoRoyaltyPool` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreoRoyaltyPool.sol ChoreoRoyaltyPool` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a rights management action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-choreo-sequence-vault-0 · dance · Choreo Sequence Vault ===== Pitch: Securely store and share choreographic sequences as immutable IPFS manifests for global access. Page: https://arcprompts.lovable.app/ideas/dance-choreo-sequence-vault-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Choreo Sequence Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely store and share choreographic sequences as immutable IPFS manifests for global access. Discipline: Dance & Choreography (dance notation archiving). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS ensures permanent, tamper-proof storage of choreographic data accessible anywhere. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreoSequenceVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentChoreoSequenceVault /// @notice Securely store and share choreographic sequences as immutable IPFS manifests for global access. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentChoreoSequenceVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreoSequenceVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreoSequenceVault.sol ChoreoSequenceVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a dance notation archiving, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-choreo-token-rewards-10 · dance · Choreo Token Rewards ===== Pitch: Reward choreographers with tokens for popular routines without requiring gas payments. Page: https://arcprompts.lovable.app/ideas/dance-choreo-token-rewards-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Choreo Token Rewards" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Reward choreographers with tokens for popular routines without requiring gas payments. Discipline: Dance & Choreography (creator incentives). Onchain primitive: Privy social wallet on Arc. Why this primitive: Embedded wallet and sponsored transactions streamline token distribution to creators. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreoTokenRewards.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogChoreoTokenRewards /// @notice Reward choreographers with tokens for popular routines without requiring gas payments. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogChoreoTokenRewards { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreoTokenRewards` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreoTokenRewards.sol ChoreoTokenRewards` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every creator incentives action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-choreochain-badge-10 · dance · ChoreoChain Badge ===== Pitch: Award NFT badges for verified choreography skills and professional credentials. Page: https://arcprompts.lovable.app/ideas/dance-choreochain-badge-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ChoreoChain Badge" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Award NFT badges for verified choreography skills and professional credentials. Discipline: Dance & Choreography (certified choreography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs provide tamperproof certification linked to creator identity and Etherscan verification. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreoChainBadge.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ChoreoChainBadge /// @notice ERC-721 provenance for: Award NFT badges for verified choreography skills and professional credentials. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ChoreoChainBadge is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ChoreoChainBadge", "CHOREO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreoChainBadge` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreoChainBadge.sol ChoreoChainBadge` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a certified choreography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-choreochain-ledger-0 · dance · ChoreoChain Ledger ===== Pitch: Securely mint and prove original dance moves as NFTs to protect choreographers' intellectual property. Page: https://arcprompts.lovable.app/ideas/dance-choreochain-ledger-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ChoreoChain Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely mint and prove original dance moves as NFTs to protect choreographers' intellectual property. Discipline: Dance & Choreography (movement cataloging). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance ensures immutable proof of original creation stored onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreoChainLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ChoreoChainLedger /// @notice ERC-721 provenance for: Securely mint and prove original dance moves as NFTs to protect choreographers' intellectual property. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ChoreoChainLedger is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ChoreoChainLedger", "CHOREO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreoChainLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreoChainLedger.sol ChoreoChainLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a movement cataloging, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-choreocraft-tokens-17 · dance · ChoreoCraft Tokens ===== Pitch: Create and sell NFT-backed custom choreography kits for teachers and studios. Page: https://arcprompts.lovable.app/ideas/dance-choreocraft-tokens-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ChoreoCraft Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create and sell NFT-backed custom choreography kits for teachers and studios. Discipline: Dance & Choreography (custom choreography kits). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT minting verifies ownership and uniqueness of choreography kit IP packages. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreoCraftTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ChoreoCraftTokens /// @notice ERC-721 provenance for: Create and sell NFT-backed custom choreography kits for teachers and studios. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ChoreoCraftTokens is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ChoreoCraftTokens", "CHOREO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreoCraftTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreoCraftTokens.sol ChoreoCraftTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a custom choreography kits, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-choreocrowd-fund-12 · dance · ChoreoCrowd Fund ===== Pitch: Enable transparent onchain fundraising for dance projects with milestones and payout automation. Page: https://arcprompts.lovable.app/ideas/dance-choreocrowd-fund-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ChoreoCrowd Fund" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable transparent onchain fundraising for dance projects with milestones and payout automation. Discipline: Dance & Choreography (project crowdfunding). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts enforce conditions and ensure proper fund distribution to creators. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreoCrowdFund.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ChoreoCrowdFund /// @notice Enable transparent onchain fundraising for dance projects with milestones and payout automation. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ChoreoCrowdFund { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreoCrowdFund` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreoCrowdFund.sol ChoreoCrowdFund` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a project crowdfunding action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-choreographer-profiles-20 · dance · Choreographer Profiles ===== Pitch: Build verified onchain profiles for choreographers with gasless updates and endorsements. Page: https://arcprompts.lovable.app/ideas/dance-choreographer-profiles-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Choreographer Profiles" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Build verified onchain profiles for choreographers with gasless updates and endorsements. Discipline: Dance & Choreography (professional branding). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social wallet plus sponsored tx streamline identity management. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreographerProfiles.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogChoreographerProfiles /// @notice Build verified onchain profiles for choreographers with gasless updates and endorsements. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogChoreographerProfiles { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreographerProfiles` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreographerProfiles.sol ChoreographerProfiles` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every professional branding action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-choreographer-s-ledger-3 · dance · Choreographer’s Ledger ===== Pitch: Record choreography permissions and usage rights as JSON manifests pinned on IPFS. Page: https://arcprompts.lovable.app/ideas/dance-choreographer-s-ledger-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Choreographer’s Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record choreography permissions and usage rights as JSON manifests pinned on IPFS. Discipline: Dance & Choreography (IP rights management). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS immutability ensures proof of IP ownership and permission records. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreographersLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentChoreographersLedger /// @notice Record choreography permissions and usage rights as JSON manifests pinned on IPFS. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentChoreographersLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreographersLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreographersLedger.sol ChoreographersLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a IP rights management, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-choreography-licensing-hub-15 · dance · Choreography Licensing Hub ===== Pitch: License dance routines with transparent blockchain contracts and gasless transactions. Page: https://arcprompts.lovable.app/ideas/dance-choreography-licensing-hub-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Choreography Licensing Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT License dance routines with transparent blockchain contracts and gasless transactions. Discipline: Dance & Choreography (rights management). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet with sponsored tx reduces licensing friction. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreographyLicensingHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogChoreographyLicensingHub /// @notice License dance routines with transparent blockchain contracts and gasless transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogChoreographyLicensingHub { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreographyLicensingHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreographyLicensingHub.sol ChoreographyLicensingHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every rights management action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-choreography-remix-log-19 · dance · Choreography Remix Log ===== Pitch: Pin JSON manifests of choreographic remixes and images on IPFS to document creative lineage. Page: https://arcprompts.lovable.app/ideas/dance-choreography-remix-log-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Choreography Remix Log" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin JSON manifests of choreographic remixes and images on IPFS to document creative lineage. Discipline: Dance & Choreography (creative versioning). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS allows immutable, traceable version histories of choreography remixes. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreographyRemixLog.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentChoreographyRemixLog /// @notice Pin JSON manifests of choreographic remixes and images on IPFS to document creative lineage. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentChoreographyRemixLog { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreographyRemixLog` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreographyRemixLog.sol ChoreographyRemixLog` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a creative versioning, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-choreominting-platform-21 · dance · ChoreoMinting Platform ===== Pitch: Empower dancers to mint and sell unique choreography NFTs directly from studios. Page: https://arcprompts.lovable.app/ideas/dance-choreominting-platform-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ChoreoMinting Platform" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Empower dancers to mint and sell unique choreography NFTs directly from studios. Discipline: Dance & Choreography (NFT creation). Onchain primitive: Arc smart contract. Why this primitive: Onchain minting supports decentralized content ownership and monetization by creators. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreoMintingPlatform.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ChoreoMintingPlatform /// @notice Empower dancers to mint and sell unique choreography NFTs directly from studios. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ChoreoMintingPlatform { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreoMintingPlatform` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreoMintingPlatform.sol ChoreoMintingPlatform` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a NFT creation action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-choreoproof-licensing-17 · dance · ChoreoProof Licensing ===== Pitch: Automate licensing agreements for choreography use with onchain smart contract enforcement. Page: https://arcprompts.lovable.app/ideas/dance-choreoproof-licensing-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ChoreoProof Licensing" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Automate licensing agreements for choreography use with onchain smart contract enforcement. Discipline: Dance & Choreography (automated contracts). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts reduce disputes by executing terms exactly as coded without intermediaries. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreoProofLicensing.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ChoreoProofLicensing /// @notice Automate licensing agreements for choreography use with onchain smart contract enforcement. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ChoreoProofLicensing { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreoProofLicensing` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreoProofLicensing.sol ChoreoProofLicensing` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a automated contracts action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-choreoswap-marketplace-14 · dance · ChoreoSwap Marketplace ===== Pitch: Swap choreography clips and movement motifs peer-to-peer as tokenized assets onchain. Page: https://arcprompts.lovable.app/ideas/dance-choreoswap-marketplace-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ChoreoSwap Marketplace" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Swap choreography clips and movement motifs peer-to-peer as tokenized assets onchain. Discipline: Dance & Choreography (content exchange). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts enable trustless, instant exchange and ownership transfer without intermediaries. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreoSwapMarketplace.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ChoreoSwapMarketplace /// @notice Swap choreography clips and movement motifs peer-to-peer as tokenized assets onchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ChoreoSwapMarketplace { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreoSwapMarketplace` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreoSwapMarketplace.sol ChoreoSwapMarketplace` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a content exchange action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-collaborative-choreo-file-7 · dance · Collaborative Choreo File ===== Pitch: Teams pin evolving choreography JSON manifests on IPFS for real-time, decentralized collaboration. Page: https://arcprompts.lovable.app/ideas/dance-collaborative-choreo-file-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Collaborative Choreo File" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Teams pin evolving choreography JSON manifests on IPFS for real-time, decentralized collaboration. Discipline: Dance & Choreography (group choreography workflow). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS allows versioned, immutable storage accessible to all team members without downtime. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CollaborativeChoreoFile.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentCollaborativeChoreoFile /// @notice Teams pin evolving choreography JSON manifests on IPFS for real-time, decentralized collaboration. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentCollaborativeChoreoFile { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CollaborativeChoreoFile` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CollaborativeChoreoFile.sol CollaborativeChoreoFile` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a group choreography workflow, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-costume-design-catalog-13 · dance · Costume Design Catalog ===== Pitch: Pin images and JSON design manifests of dance costumes on IPFS for long-term design preservation. Page: https://arcprompts.lovable.app/ideas/dance-costume-design-catalog-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Costume Design Catalog" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin images and JSON design manifests of dance costumes on IPFS for long-term design preservation. Discipline: Dance & Choreography (dance wardrobe archives). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS provides immutable, decentralized storage ensuring no loss of costume records. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CostumeDesignCatalog.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentCostumeDesignCatalog /// @notice Pin images and JSON design manifests of dance costumes on IPFS for long-term design preservation. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentCostumeDesignCatalog { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CostumeDesignCatalog` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CostumeDesignCatalog.sol CostumeDesignCatalog` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a dance wardrobe archives, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-crowddance-licensing-23 · dance · CrowdDance Licensing ===== Pitch: License remixed choreography created by crowdsourced dancers with onchain rights management. Page: https://arcprompts.lovable.app/ideas/dance-crowddance-licensing-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "CrowdDance Licensing" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT License remixed choreography created by crowdsourced dancers with onchain rights management. Discipline: Dance & Choreography (mass collaboration). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts coordinate multi-owner rights and revenue splitting efficiently and fairly. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CrowdDanceLicensing.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title CrowdDanceLicensing /// @notice License remixed choreography created by crowdsourced dancers with onchain rights management. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CrowdDanceLicensing { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CrowdDanceLicensing` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CrowdDanceLicensing.sol CrowdDanceLicensing` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a mass collaboration action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dance-challenge-repository-15 · dance · Dance Challenge Repository ===== Pitch: Pin community dance challenge videos and JSON instructions on IPFS for global participation. Page: https://arcprompts.lovable.app/ideas/dance-dance-challenge-repository-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dance Challenge Repository" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin community dance challenge videos and JSON instructions on IPFS for global participation. Discipline: Dance & Choreography (community choreography). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS enables permanent, censorship-resistant hosting of viral dance challenges globally. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceChallengeRepository.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentDanceChallengeRepository /// @notice Pin community dance challenge videos and JSON instructions on IPFS for global participation. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentDanceChallengeRepository { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceChallengeRepository` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceChallengeRepository.sol DanceChallengeRepository` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a community choreography, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dance-event-tickets-6 · dance · Dance Event Tickets ===== Pitch: Sell and distribute dance event tickets as NFTs with no gas for buyers. Page: https://arcprompts.lovable.app/ideas/dance-dance-event-tickets-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dance Event Tickets" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Sell and distribute dance event tickets as NFTs with no gas for buyers. Discipline: Dance & Choreography (ticketing system). Onchain primitive: Privy social wallet on Arc. Why this primitive: Embedded wallet bootstrapped and sponsored tx reduce friction in ticket ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceEventTickets.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogDanceEventTickets /// @notice Sell and distribute dance event tickets as NFTs with no gas for buyers. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogDanceEventTickets { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceEventTickets` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceEventTickets.sol DanceEventTickets` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every ticketing system action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dance-festival-archive-12 · dance · Dance Festival Archive ===== Pitch: Pin photos and JSON manifests of live dance festival performances on IPFS for posterity and access. Page: https://arcprompts.lovable.app/ideas/dance-dance-festival-archive-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dance Festival Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin photos and JSON manifests of live dance festival performances on IPFS for posterity and access. Discipline: Dance & Choreography (event documentation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata enables permanent, decentralized storage preventing loss of live event records. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceFestivalArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentDanceFestivalArchive /// @notice Pin photos and JSON manifests of live dance festival performances on IPFS for posterity and access. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentDanceFestivalArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceFestivalArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceFestivalArchive.sol DanceFestivalArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a event documentation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dance-gear-marketplace-11 · dance · Dance Gear Marketplace ===== Pitch: Buy and sell dance apparel and gear as NFTs with gasless transaction experience. Page: https://arcprompts.lovable.app/ideas/dance-dance-gear-marketplace-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dance Gear Marketplace" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Buy and sell dance apparel and gear as NFTs with gasless transaction experience. Discipline: Dance & Choreography (digital commerce). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social wallet and sponsored tx enable smooth gas-free commerce. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceGearMarketplace.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogDanceGearMarketplace /// @notice Buy and sell dance apparel and gear as NFTs with gasless transaction experience. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogDanceGearMarketplace { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceGearMarketplace` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceGearMarketplace.sol DanceGearMarketplace` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every digital commerce action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dance-move-provenance-8 · dance · Dance Move Provenance ===== Pitch: Verify authenticity and origin of iconic dance moves onchain, gaslessly. Page: https://arcprompts.lovable.app/ideas/dance-dance-move-provenance-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dance Move Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Verify authenticity and origin of iconic dance moves onchain, gaslessly. Discipline: Dance & Choreography (authenticity verification). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored tx allow users to prove originality without gas costs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceMoveProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogDanceMoveProvenance /// @notice Verify authenticity and origin of iconic dance moves onchain, gaslessly. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogDanceMoveProvenance { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceMoveProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceMoveProvenance.sol DanceMoveProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every authenticity verification action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dance-move-tokens-8 · dance · Dance Move Tokens ===== Pitch: Create, pin, and trade unique dance moves with JSON metadata on IPFS representing rights. Page: https://arcprompts.lovable.app/ideas/dance-dance-move-tokens-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dance Move Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create, pin, and trade unique dance moves with JSON metadata on IPFS representing rights. Discipline: Dance & Choreography (movement licensing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata facilitates permanent off-chain storage of token metadata ensuring rights transparency. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceMoveTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentDanceMoveTokens /// @notice Create, pin, and trade unique dance moves with JSON metadata on IPFS representing rights. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentDanceMoveTokens { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceMoveTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceMoveTokens.sol DanceMoveTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a movement licensing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dance-nft-gallery-14 · dance · Dance NFT Gallery ===== Pitch: Host curated dance NFT galleries that users can explore without paying gas. Page: https://arcprompts.lovable.app/ideas/dance-dance-nft-gallery-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dance NFT Gallery" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Host curated dance NFT galleries that users can explore without paying gas. Discipline: Dance & Choreography (exhibition curation). Onchain primitive: Privy social wallet on Arc. Why this primitive: Google sign-in based wallet plus sponsored tx simplify visitor experience. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceNFTGallery.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogDanceNFTGallery /// @notice Host curated dance NFT galleries that users can explore without paying gas. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogDanceNFTGallery { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceNFTGallery` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceNFTGallery.sol DanceNFTGallery` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every exhibition curation action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dance-nft-gallery-2 · dance · Dance NFT Gallery ===== Pitch: Artists mint and pin digital dance art portfolios as NFTs stored forever on IPFS. Page: https://arcprompts.lovable.app/ideas/dance-dance-nft-gallery-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dance NFT Gallery" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Artists mint and pin digital dance art portfolios as NFTs stored forever on IPFS. Discipline: Dance & Choreography (digital dance art). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s IPFS pinning guarantees permanent availability of NFT metadata and artworks. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceNFTGallery.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentDanceNFTGallery /// @notice Artists mint and pin digital dance art portfolios as NFTs stored forever on IPFS. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentDanceNFTGallery { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceNFTGallery` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceNFTGallery.sol DanceNFTGallery` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a digital dance art, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dance-nft-streaming-21 · dance · Dance NFT Streaming ===== Pitch: Stream exclusive dance content NFTs to fans without requiring gas payments. Page: https://arcprompts.lovable.app/ideas/dance-dance-nft-streaming-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dance NFT Streaming" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Stream exclusive dance content NFTs to fans without requiring gas payments. Discipline: Dance & Choreography (content distribution). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet and sponsored tx allow smooth access to paid streams. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceNFTStreaming.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogDanceNFTStreaming /// @notice Stream exclusive dance content NFTs to fans without requiring gas payments. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogDanceNFTStreaming { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceNFTStreaming` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceNFTStreaming.sol DanceNFTStreaming` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every content distribution action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dance-nft-tickets-1 · dance · Dance NFT Tickets ===== Pitch: Sell and verify exclusive dance event tickets as tradable NFTs on-chain to prevent fraud. Page: https://arcprompts.lovable.app/ideas/dance-dance-nft-tickets-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dance NFT Tickets" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Sell and verify exclusive dance event tickets as tradable NFTs on-chain to prevent fraud. Discipline: Dance & Choreography (event access control). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts automate issuance and verification of NFT tickets without intermediaries. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceNFTTickets.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DanceNFTTickets /// @notice Sell and verify exclusive dance event tickets as tradable NFTs on-chain to prevent fraud. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DanceNFTTickets { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceNFTTickets` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceNFTTickets.sol DanceNFTTickets` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a event access control action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dance-props-inventory-20 · dance · Dance Props Inventory ===== Pitch: Pin images and JSON manifests of dance prop inventories to IPFS for decentralized sharing. Page: https://arcprompts.lovable.app/ideas/dance-dance-props-inventory-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dance Props Inventory" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin images and JSON manifests of dance prop inventories to IPFS for decentralized sharing. Discipline: Dance & Choreography (production resource management). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS guarantees permanent, accessible storage of production resource data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DancePropsInventory.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentDancePropsInventory /// @notice Pin images and JSON manifests of dance prop inventories to IPFS for decentralized sharing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentDancePropsInventory { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DancePropsInventory` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DancePropsInventory.sol DancePropsInventory` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a production resource management, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dance-studio-portfolio-18 · dance · Dance Studio Portfolio ===== Pitch: Studios pin portfolios of images and JSON class manifests on IPFS to showcase offerings forever. Page: https://arcprompts.lovable.app/ideas/dance-dance-studio-portfolio-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dance Studio Portfolio" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Studios pin portfolios of images and JSON class manifests on IPFS to showcase offerings forever. Discipline: Dance & Choreography (business marketing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS secures studio marketing materials with permanent, decentralized hosting. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceStudioPortfolio.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentDanceStudioPortfolio /// @notice Studios pin portfolios of images and JSON class manifests on IPFS to showcase offerings forever. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentDanceStudioPortfolio { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceStudioPortfolio` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceStudioPortfolio.sol DanceStudioPortfolio` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a business marketing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dance-therapy-logs-22 · dance · Dance Therapy Logs ===== Pitch: Pin JSON and image records of dance therapy sessions on IPFS for secure patient histories. Page: https://arcprompts.lovable.app/ideas/dance-dance-therapy-logs-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dance Therapy Logs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin JSON and image records of dance therapy sessions on IPFS for secure patient histories. Discipline: Dance & Choreography (health documentation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS ensures confidential, permanent, and decentralized health session storage. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceTherapyLogs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentDanceTherapyLogs /// @notice Pin JSON and image records of dance therapy sessions on IPFS for secure patient histories. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentDanceTherapyLogs { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceTherapyLogs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceTherapyLogs.sol DanceTherapyLogs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a health documentation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dancedao-collective-7 · dance · DanceDAO Collective ===== Pitch: Enable dance groups to govern studios and projects democratically via onchain voting. Page: https://arcprompts.lovable.app/ideas/dance-dancedao-collective-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "DanceDAO Collective" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable dance groups to govern studios and projects democratically via onchain voting. Discipline: Dance & Choreography (community governance). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts ensure transparent, tamper-proof decentralized decision-making. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceDAOCollective.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DanceDAOCollective /// @notice Enable dance groups to govern studios and projects democratically via onchain voting. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DanceDAOCollective { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceDAOCollective` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceDAOCollective.sol DanceDAOCollective` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a community governance action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dancedna-registry-11 · dance · DanceDNA Registry ===== Pitch: Mint NFTs encoding a dancer’s unique style ‘DNA’ for branding and licensing. Page: https://arcprompts.lovable.app/ideas/dance-dancedna-registry-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "DanceDNA Registry" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs encoding a dancer’s unique style ‘DNA’ for branding and licensing. Discipline: Dance & Choreography (personal style encoding). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs link personal style data stored on IPFS to a verifiable creator token. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceDNARegistry.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title DanceDNARegistry /// @notice ERC-721 provenance for: Mint NFTs encoding a dancer’s unique style ‘DNA’ for branding and licensing. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DanceDNARegistry is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("DanceDNARegistry", "DANCED") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceDNARegistry` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceDNARegistry.sol DanceDNARegistry` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a personal style encoding, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-danceledger-archives-20 · dance · DanceLedger Archives ===== Pitch: Archive dance event records and performances permanently on a public blockchain ledger. Page: https://arcprompts.lovable.app/ideas/dance-danceledger-archives-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "DanceLedger Archives" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Archive dance event records and performances permanently on a public blockchain ledger. Discipline: Dance & Choreography (historical record). Onchain primitive: Arc smart contract. Why this primitive: Immutable data storage preserves cultural heritage accessible globally forever. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceLedgerArchives.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DanceLedgerArchives /// @notice Archive dance event records and performances permanently on a public blockchain ledger. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DanceLedgerArchives { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceLedgerArchives` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceLedgerArchives.sol DanceLedgerArchives` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a historical record action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dancemove-provenance-10 · dance · DanceMove Provenance ===== Pitch: Trace and display the lineage and remix history of dance moves on an immutable ledger. Page: https://arcprompts.lovable.app/ideas/dance-dancemove-provenance-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "DanceMove Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Trace and display the lineage and remix history of dance moves on an immutable ledger. Discipline: Dance & Choreography (creative lineage). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts track ownership and modification history transparently and permanently. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceMoveProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DanceMoveProvenance /// @notice Trace and display the lineage and remix history of dance moves on an immutable ledger. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DanceMoveProvenance { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceMoveProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceMoveProvenance.sol DanceMoveProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a creative lineage action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dancestep-identity-13 · dance · DanceStep Identity ===== Pitch: Create verifiable digital identities for dancers' skills and experiences stored onchain. Page: https://arcprompts.lovable.app/ideas/dance-dancestep-identity-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "DanceStep Identity" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create verifiable digital identities for dancers' skills and experiences stored onchain. Discipline: Dance & Choreography (digital identity). Onchain primitive: Arc smart contract. Why this primitive: Immutable smart contracts secure skill records accessible globally by studios and agents. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceStepIdentity.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DanceStepIdentity /// @notice Create verifiable digital identities for dancers' skills and experiences stored onchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DanceStepIdentity { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceStepIdentity` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceStepIdentity.sol DanceStepIdentity` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a digital identity action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-danceswap-collaborations-24 · dance · DanceSwap Collaborations ===== Pitch: Facilitate peer-to-peer choreography collaboration and revenue sharing via smart contracts. Page: https://arcprompts.lovable.app/ideas/dance-danceswap-collaborations-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "DanceSwap Collaborations" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Facilitate peer-to-peer choreography collaboration and revenue sharing via smart contracts. Discipline: Dance & Choreography (peer collaboration). Onchain primitive: Arc smart contract. Why this primitive: Onchain contracts ensure trustless cooperation and fair profit distribution among artists. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceSwapCollaborations.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DanceSwapCollaborations /// @notice Facilitate peer-to-peer choreography collaboration and revenue sharing via smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DanceSwapCollaborations { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceSwapCollaborations` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceSwapCollaborations.sol DanceSwapCollaborations` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a peer collaboration action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dancetoken-staking-18 · dance · DanceToken Staking ===== Pitch: Stake tokens to support favorite dancers and receive exclusive perks or content rewards. Page: https://arcprompts.lovable.app/ideas/dance-dancetoken-staking-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "DanceToken Staking" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Stake tokens to support favorite dancers and receive exclusive perks or content rewards. Discipline: Dance & Choreography (community incentives). Onchain primitive: Arc smart contract. Why this primitive: Onchain staking programs enable transparent, programmable community funding and rewards. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceTokenStaking.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DanceTokenStaking /// @notice Stake tokens to support favorite dancers and receive exclusive perks or content rewards. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DanceTokenStaking { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceTokenStaking` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceTokenStaking.sol DanceTokenStaking` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a community incentives action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-dancetrail-provenance-18 · dance · DanceTrail Provenance ===== Pitch: Record and mint NFT trails of live performances to authenticate originality and authorship. Page: https://arcprompts.lovable.app/ideas/dance-dancetrail-provenance-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "DanceTrail Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record and mint NFT trails of live performances to authenticate originality and authorship. Discipline: Dance & Choreography (performance recording). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs link performance data stored on IPFS with immutable onchain proof. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DanceTrailProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title DanceTrailProvenance /// @notice ERC-721 provenance for: Record and mint NFT trails of live performances to authenticate originality and authorship. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DanceTrailProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("DanceTrailProvenance", "DANCET") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DanceTrailProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DanceTrailProvenance.sol DanceTrailProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a performance recording, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-echodance-ledger-8 · dance · EchoDance Ledger ===== Pitch: Tokenize remix rights of dance routines as NFTs to facilitate fair use and royalties. Page: https://arcprompts.lovable.app/ideas/dance-echodance-ledger-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "EchoDance Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Tokenize remix rights of dance routines as NFTs to facilitate fair use and royalties. Discipline: Dance & Choreography (dance remix rights). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance enables transparent chain of rights and remix permissions. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EchoDanceLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title EchoDanceLedger /// @notice ERC-721 provenance for: Tokenize remix rights of dance routines as NFTs to facilitate fair use and royalties. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract EchoDanceLedger is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("EchoDanceLedger", "ECHODA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EchoDanceLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EchoDanceLedger.sol EchoDanceLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a dance remix rights, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-flowmotion-archive-1 · dance · FlowMotion Archive ===== Pitch: Mint NFTs representing detailed dance notations for preservation and easy sharing among dancers. Page: https://arcprompts.lovable.app/ideas/dance-flowmotion-archive-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FlowMotion Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs representing detailed dance notations for preservation and easy sharing among dancers. Discipline: Dance & Choreography (dance notation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs anchor IPFS-stored notation files with verifiable onchain ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FlowMotionArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title FlowMotionArchive /// @notice ERC-721 provenance for: Mint NFTs representing detailed dance notations for preservation and easy sharing among dancers. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FlowMotionArchive is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("FlowMotionArchive", "FLOWMO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FlowMotionArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FlowMotionArchive.sol FlowMotionArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a dance notation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-freezeframe-tokens-15 · dance · FreezeFrame Tokens ===== Pitch: Mint NFTs capturing iconic frozen dance frames for fan engagement and merchandising. Page: https://arcprompts.lovable.app/ideas/dance-freezeframe-tokens-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FreezeFrame Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs capturing iconic frozen dance frames for fan engagement and merchandising. Discipline: Dance & Choreography (iconic frozen poses). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs provide proof of authenticity and scarcity of digitally minted poses. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FreezeFrameTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title FreezeFrameTokens /// @notice ERC-721 provenance for: Mint NFTs capturing iconic frozen dance frames for fan engagement and merchandising. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FreezeFrameTokens is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("FreezeFrameTokens", "FREEZE") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FreezeFrameTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FreezeFrameTokens.sol FreezeFrameTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a iconic frozen poses, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-gas-free-dance-nfts-16 · dance · Gas-Free Dance NFTs ===== Pitch: Mint and trade unique dance moment NFTs without gas fees for fans and artists. Page: https://arcprompts.lovable.app/ideas/dance-gas-free-dance-nfts-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gas-Free Dance NFTs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint and trade unique dance moment NFTs without gas fees for fans and artists. Discipline: Dance & Choreography (blockchain collectibles). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet and sponsored transactions eliminate gas barriers for casual users. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GasFreeDanceNFTs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGasFreeDanceNFTs /// @notice Mint and trade unique dance moment NFTs without gas fees for fans and artists. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGasFreeDanceNFTs { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GasFreeDanceNFTs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GasFreeDanceNFTs.sol GasFreeDanceNFTs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every blockchain collectibles action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-gas-free-dance-voting-4 · dance · Gas-Free Dance Voting ===== Pitch: Host dance community polls and awards with zero gas cost for voters. Page: https://arcprompts.lovable.app/ideas/dance-gas-free-dance-voting-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gas-Free Dance Voting" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Host dance community polls and awards with zero gas cost for voters. Discipline: Dance & Choreography (community decision-making). Onchain primitive: Privy social wallet on Arc. Why this primitive: Google sign-in and sponsored tx streamline inclusive community governance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GasFreeDanceVoting.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGasFreeDanceVoting /// @notice Host dance community polls and awards with zero gas cost for voters. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGasFreeDanceVoting { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GasFreeDanceVoting` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GasFreeDanceVoting.sol GasFreeDanceVoting` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every community decision-making action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-gas-free-dance-workshops-9 · dance · Gas-Free Dance Workshops ===== Pitch: Enroll and attend dance workshops with blockchain-secured certificates, no gas fees. Page: https://arcprompts.lovable.app/ideas/dance-gas-free-dance-workshops-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gas-Free Dance Workshops" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enroll and attend dance workshops with blockchain-secured certificates, no gas fees. Discipline: Dance & Choreography (online learning). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet and sponsored tx improve user onboarding and certificate issuance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GasFreeDanceWorkshops.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGasFreeDanceWorkshops /// @notice Enroll and attend dance workshops with blockchain-secured certificates, no gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGasFreeDanceWorkshops { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GasFreeDanceWorkshops` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GasFreeDanceWorkshops.sol GasFreeDanceWorkshops` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every online learning action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-gasless-dance-analytics-23 · dance · Gasless Dance Analytics ===== Pitch: Access detailed dance performance analytics with blockchain-backed data, no gas required. Page: https://arcprompts.lovable.app/ideas/dance-gasless-dance-analytics-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Dance Analytics" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Access detailed dance performance analytics with blockchain-backed data, no gas required. Discipline: Dance & Choreography (performance insights). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet plus sponsored tx facilitate data queries at zero user cost. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessDanceAnalytics.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessDanceAnalytics /// @notice Access detailed dance performance analytics with blockchain-backed data, no gas required. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessDanceAnalytics { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessDanceAnalytics` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessDanceAnalytics.sol GaslessDanceAnalytics` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every performance insights action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-gasless-dance-battles-0 · dance · Gasless Dance Battles ===== Pitch: Enable dancers to join and sponsor dance battles without paying gas fees. Page: https://arcprompts.lovable.app/ideas/dance-gasless-dance-battles-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Dance Battles" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable dancers to join and sponsor dance battles without paying gas fees. Discipline: Dance & Choreography (competitive choreography). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social with sponsored tx removes friction of blockchain fees in quick peer competitions. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessDanceBattles.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessDanceBattles /// @notice Enable dancers to join and sponsor dance battles without paying gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessDanceBattles { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessDanceBattles` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessDanceBattles.sol GaslessDanceBattles` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every competitive choreography action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-gasless-dance-challenges-13 · dance · Gasless Dance Challenges ===== Pitch: Launch viral dance challenges with onchain proof and no gas cost for participants. Page: https://arcprompts.lovable.app/ideas/dance-gasless-dance-challenges-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Dance Challenges" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Launch viral dance challenges with onchain proof and no gas cost for participants. Discipline: Dance & Choreography (social engagement). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet and sponsored transactions maximize participation by eliminating fees. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessDanceChallenges.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessDanceChallenges /// @notice Launch viral dance challenges with onchain proof and no gas cost for participants. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessDanceChallenges { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessDanceChallenges` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessDanceChallenges.sol GaslessDanceChallenges` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every social engagement action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-gasless-dance-royalties-7 · dance · Gasless Dance Royalties ===== Pitch: Pay royalties to dancers and choreographers instantly with no transaction fees. Page: https://arcprompts.lovable.app/ideas/dance-gasless-dance-royalties-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Dance Royalties" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pay royalties to dancers and choreographers instantly with no transaction fees. Discipline: Dance & Choreography (automatic payments). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored transactions ensure fast, costless royalty distribution via Privy wallet. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessDanceRoyalties.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessDanceRoyalties /// @notice Pay royalties to dancers and choreographers instantly with no transaction fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessDanceRoyalties { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessDanceRoyalties` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessDanceRoyalties.sol GaslessDanceRoyalties` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every automatic payments action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-gasless-dance-socials-19 · dance · Gasless Dance Socials ===== Pitch: Create dance-focused social networks with token rewards and no gas transaction fees. Page: https://arcprompts.lovable.app/ideas/dance-gasless-dance-socials-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Dance Socials" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create dance-focused social networks with token rewards and no gas transaction fees. Discipline: Dance & Choreography (community building). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet and sponsored tx drive engagement with zero cost actions. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessDanceSocials.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessDanceSocials /// @notice Create dance-focused social networks with token rewards and no gas transaction fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessDanceSocials { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessDanceSocials` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessDanceSocials.sol GaslessDanceSocials` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every community building action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-glideproof-tokens-22 · dance · GlideProof Tokens ===== Pitch: Mint NFTs encoding fluidity metrics of dance sequences for analytics and coaching. Page: https://arcprompts.lovable.app/ideas/dance-glideproof-tokens-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "GlideProof Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs encoding fluidity metrics of dance sequences for analytics and coaching. Discipline: Dance & Choreography (fluidity measurement). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs timestamp and verify data stored offchain, linked to creator identities. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GlideProofTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title GlideProofTokens /// @notice ERC-721 provenance for: Mint NFTs encoding fluidity metrics of dance sequences for analytics and coaching. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract GlideProofTokens is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("GlideProofTokens", "GLIDEP") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GlideProofTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GlideProofTokens.sol GlideProofTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a fluidity measurement, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-global-dance-archive-6 · dance · Global Dance Archive ===== Pitch: Digitize and pin cultural dance archives on IPFS preserving global dance heritage forever. Page: https://arcprompts.lovable.app/ideas/dance-global-dance-archive-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Global Dance Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Digitize and pin cultural dance archives on IPFS preserving global dance heritage forever. Discipline: Dance & Choreography (dance heritage preservation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s IPFS ensures permanent, decentralized custody of invaluable dance history data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GlobalDanceArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentGlobalDanceArchive /// @notice Digitize and pin cultural dance archives on IPFS preserving global dance heritage forever. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentGlobalDanceArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GlobalDanceArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GlobalDanceArchive.sol GlobalDanceArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a dance heritage preservation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-interactive-dance-maps-10 · dance · Interactive Dance Maps ===== Pitch: Pin visual and JSON data of choreography spatial maps on IPFS for decentralized access and reuse. Page: https://arcprompts.lovable.app/ideas/dance-interactive-dance-maps-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Interactive Dance Maps" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin visual and JSON data of choreography spatial maps on IPFS for decentralized access and reuse. Discipline: Dance & Choreography (movement spatialization). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS stores complex multimedia choreography spatializations without central failures. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InteractiveDanceMaps.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentInteractiveDanceMaps /// @notice Pin visual and JSON data of choreography spatial maps on IPFS for decentralized access and reuse. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentInteractiveDanceMaps { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InteractiveDanceMaps` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InteractiveDanceMaps.sol InteractiveDanceMaps` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a movement spatialization, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-interactive-step-manuals-23 · dance · Interactive Step Manuals ===== Pitch: Pin interactive dance step image guides plus JSON instructions on IPFS for teacher-student use. Page: https://arcprompts.lovable.app/ideas/dance-interactive-step-manuals-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Interactive Step Manuals" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin interactive dance step image guides plus JSON instructions on IPFS for teacher-student use. Discipline: Dance & Choreography (teaching aids). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS supports permanent, decentralized hosting of teaching content accessible offline. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InteractiveStepManuals.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentInteractiveStepManuals /// @notice Pin interactive dance step image guides plus JSON instructions on IPFS for teacher-student use. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentInteractiveStepManuals { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InteractiveStepManuals` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InteractiveStepManuals.sol InteractiveStepManuals` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a teaching aids, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-leapchain-ledger-24 · dance · LeapChain Ledger ===== Pitch: Mint NFTs of signature leaps to prove originality and enable commercial licensing. Page: https://arcprompts.lovable.app/ideas/dance-leapchain-ledger-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LeapChain Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs of signature leaps to prove originality and enable commercial licensing. Discipline: Dance & Choreography (leaps and jumps). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs anchor leap moves with immutable creator provenance onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LeapChainLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title LeapChainLedger /// @notice ERC-721 provenance for: Mint NFTs of signature leaps to prove originality and enable commercial licensing. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract LeapChainLedger is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("LeapChainLedger", "LEAPCH") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LeapChainLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LeapChainLedger.sol LeapChainLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a leaps and jumps, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-live-choreo-snapshot-17 · dance · Live Choreo Snapshot ===== Pitch: Pin image and JSON snapshots of live choreography moments on IPFS for historical timestamping. Page: https://arcprompts.lovable.app/ideas/dance-live-choreo-snapshot-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Live Choreo Snapshot" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin image and JSON snapshots of live choreography moments on IPFS for historical timestamping. Discipline: Dance & Choreography (performance capture). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS ensures permanent, verifiable storage of live performance digital artifacts. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LiveChoreoSnapshot.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentLiveChoreoSnapshot /// @notice Pin image and JSON snapshots of live choreography moments on IPFS for historical timestamping. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentLiveChoreoSnapshot { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LiveChoreoSnapshot` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LiveChoreoSnapshot.sol LiveChoreoSnapshot` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a performance capture, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-livemotion-payments-11 · dance · LiveMotion Payments ===== Pitch: Settle payments instantly for live dance services and gigs through smart contracts. Page: https://arcprompts.lovable.app/ideas/dance-livemotion-payments-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LiveMotion Payments" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Settle payments instantly for live dance services and gigs through smart contracts. Discipline: Dance & Choreography (instant settlement). Onchain primitive: Arc smart contract. Why this primitive: Onchain escrow and release ensures trustless, real-time transaction finality. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LiveMotionPayments.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title LiveMotionPayments /// @notice Settle payments instantly for live dance services and gigs through smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract LiveMotionPayments { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LiveMotionPayments` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LiveMotionPayments.sol LiveMotionPayments` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a instant settlement action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-mirrormove-vault-5 · dance · MirrorMove Vault ===== Pitch: NFT dance movement tokens enable secure sharing and replication tracking of signature moves. Page: https://arcprompts.lovable.app/ideas/dance-mirrormove-vault-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MirrorMove Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT NFT dance movement tokens enable secure sharing and replication tracking of signature moves. Discipline: Dance & Choreography (movement replication). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs offer tamper-proof proof of movement origin and transfer onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MirrorMoveVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MirrorMoveVault /// @notice ERC-721 provenance for: NFT dance movement tokens enable secure sharing and replication tracking of signature moves. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MirrorMoveVault is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MirrorMoveVault", "MIRROR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MirrorMoveVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MirrorMoveVault.sol MirrorMoveVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a movement replication, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-motionflow-tokens-7 · dance · MotionFlow Tokens ===== Pitch: Mint NFTs of continuous dance flows that authenticate choreography progression and originality. Page: https://arcprompts.lovable.app/ideas/dance-motionflow-tokens-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MotionFlow Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs of continuous dance flows that authenticate choreography progression and originality. Discipline: Dance & Choreography (dance flow sequences). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs create unalterable records of flow sequences onchain for proof of authorship. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MotionFlowTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MotionFlowTokens /// @notice ERC-721 provenance for: Mint NFTs of continuous dance flows that authenticate choreography progression and originality. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MotionFlowTokens is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MotionFlowTokens", "MOTION") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MotionFlowTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MotionFlowTokens.sol MotionFlowTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a dance flow sequences, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-movecoin-rewards-2 · dance · MoveCoin Rewards ===== Pitch: Reward dancers with crypto tokens for participation and achievements in studio classes. Page: https://arcprompts.lovable.app/ideas/dance-movecoin-rewards-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MoveCoin Rewards" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Reward dancers with crypto tokens for participation and achievements in studio classes. Discipline: Dance & Choreography (incentive tokenization). Onchain primitive: Arc smart contract. Why this primitive: Onchain tokens enable transparent, automatic reward distribution and tracking. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MoveCoinRewards.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title MoveCoinRewards /// @notice Reward dancers with crypto tokens for participation and achievements in studio classes. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MoveCoinRewards { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MoveCoinRewards` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MoveCoinRewards.sol MoveCoinRewards` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a incentive tokenization action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-movemark-licensing-14 · dance · MoveMark Licensing ===== Pitch: License choreography sequences securely as NFTs to streamline usage tracking and payments. Page: https://arcprompts.lovable.app/ideas/dance-movemark-licensing-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MoveMark Licensing" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT License choreography sequences securely as NFTs to streamline usage tracking and payments. Discipline: Dance & Choreography (choreography licensing). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance ensures transparent onchain tracking of licensing rights and transfers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MoveMarkLicensing.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MoveMarkLicensing /// @notice ERC-721 provenance for: License choreography sequences securely as NFTs to streamline usage tracking and payments. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MoveMarkLicensing is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MoveMarkLicensing", "MOVEMA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MoveMarkLicensing` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MoveMarkLicensing.sol MoveMarkLicensing` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a choreography licensing, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-movement-annotation-tool-21 · dance · Movement Annotation Tool ===== Pitch: Pin annotated movement images and JSON metadata on IPFS to support global dance research. Page: https://arcprompts.lovable.app/ideas/dance-movement-annotation-tool-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Movement Annotation Tool" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin annotated movement images and JSON metadata on IPFS to support global dance research. Discipline: Dance & Choreography (dance research). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS provides immutable, globally accessible data for academic and artistic analysis. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MovementAnnotationTool.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMovementAnnotationTool /// @notice Pin annotated movement images and JSON metadata on IPFS to support global dance research. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMovementAnnotationTool { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MovementAnnotationTool` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MovementAnnotationTool.sol MovementAnnotationTool` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a dance research, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-movement-data-wallet-12 · dance · Movement Data Wallet ===== Pitch: Securely store and share movement data NFTs with no blockchain fees for dancers. Page: https://arcprompts.lovable.app/ideas/dance-movement-data-wallet-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Movement Data Wallet" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely store and share movement data NFTs with no blockchain fees for dancers. Discipline: Dance & Choreography (performance analytics). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet and sponsored tx ensure easy access and sharing of data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MovementDataWallet.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogMovementDataWallet /// @notice Securely store and share movement data NFTs with no blockchain fees for dancers. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogMovementDataWallet { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MovementDataWallet` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MovementDataWallet.sol MovementDataWallet` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every performance analytics action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-movement-emotion-tags-14 · dance · Movement Emotion Tags ===== Pitch: Pin emotionally tagged dance movement metadata on IPFS to enhance creative interpretative tools. Page: https://arcprompts.lovable.app/ideas/dance-movement-emotion-tags-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Movement Emotion Tags" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin emotionally tagged dance movement metadata on IPFS to enhance creative interpretative tools. Discipline: Dance & Choreography (affective annotation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS stores richly annotated JSON ensuring permanent availability for emotion-based indexing. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MovementEmotionTags.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMovementEmotionTags /// @notice Pin emotionally tagged dance movement metadata on IPFS to enhance creative interpretative tools. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMovementEmotionTags { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MovementEmotionTags` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MovementEmotionTags.sol MovementEmotionTags` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a affective annotation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-movement-metadata-mapper-5 · dance · Movement Metadata Mapper ===== Pitch: Attach rich JSON metadata to dance performance images pinned via IPFS for enhanced searchability. Page: https://arcprompts.lovable.app/ideas/dance-movement-metadata-mapper-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Movement Metadata Mapper" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Attach rich JSON metadata to dance performance images pinned via IPFS for enhanced searchability. Discipline: Dance & Choreography (dance data annotation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS stores immutable annotated datasets accessible globally without central servers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MovementMetadataMapper.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMovementMetadataMapper /// @notice Attach rich JSON metadata to dance performance images pinned via IPFS for enhanced searchability. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMovementMetadataMapper { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MovementMetadataMapper` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MovementMetadataMapper.sol MovementMetadataMapper` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a dance data annotation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-movement-moodboard-1 · dance · Movement Moodboard ===== Pitch: Create and pin curated dance inspiration boards with images and JSON metadata on IPFS. Page: https://arcprompts.lovable.app/ideas/dance-movement-moodboard-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Movement Moodboard" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create and pin curated dance inspiration boards with images and JSON metadata on IPFS. Discipline: Dance & Choreography (dance inspiration boards). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT uploads enable decentralized and permanent preservation of moodboards. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MovementMoodboard.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMovementMoodboard /// @notice Create and pin curated dance inspiration boards with images and JSON metadata on IPFS. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMovementMoodboard { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MovementMoodboard` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MovementMoodboard.sol MovementMoodboard` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a dance inspiration boards, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-movement-patent-registry-24 · dance · Movement Patent Registry ===== Pitch: Pin JSON manifests documenting novel dance moves on IPFS to certify original creation dates. Page: https://arcprompts.lovable.app/ideas/dance-movement-patent-registry-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Movement Patent Registry" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin JSON manifests documenting novel dance moves on IPFS to certify original creation dates. Discipline: Dance & Choreography (innovation tracking). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS provides immutable timestamped records critical for innovation proofing. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MovementPatentRegistry.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMovementPatentRegistry /// @notice Pin JSON manifests documenting novel dance moves on IPFS to certify original creation dates. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMovementPatentRegistry { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MovementPatentRegistry` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MovementPatentRegistry.sol MovementPatentRegistry` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a innovation tracking, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-movement-royalty-tracker-3 · dance · Movement Royalty Tracker ===== Pitch: Automatically track and reward choreographers when their moves get used commercially. Page: https://arcprompts.lovable.app/ideas/dance-movement-royalty-tracker-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Movement Royalty Tracker" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Automatically track and reward choreographers when their moves get used commercially. Discipline: Dance & Choreography (usage rights). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored tx enable transparent royalty distribution without gas burden. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MovementRoyaltyTracker.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogMovementRoyaltyTracker /// @notice Automatically track and reward choreographers when their moves get used commercially. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogMovementRoyaltyTracker { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MovementRoyaltyTracker` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MovementRoyaltyTracker.sol MovementRoyaltyTracker` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every usage rights action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-movement-style-index-16 · dance · Movement Style Index ===== Pitch: Pin genre-classified dance move datasets with JSON metadata on IPFS for style machine parsing. Page: https://arcprompts.lovable.app/ideas/dance-movement-style-index-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Movement Style Index" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin genre-classified dance move datasets with JSON metadata on IPFS for style machine parsing. Discipline: Dance & Choreography (genre classification). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS stores large stylistic datasets immutably accessible for genre recognition algorithms. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MovementStyleIndex.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMovementStyleIndex /// @notice Pin genre-classified dance move datasets with JSON metadata on IPFS for style machine parsing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMovementStyleIndex { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MovementStyleIndex` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MovementStyleIndex.sol MovementStyleIndex` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a genre classification, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-movement-token-tips-22 · dance · Movement Token Tips ===== Pitch: Let fans tip dancers instantly with tokens and no gas fees. Page: https://arcprompts.lovable.app/ideas/dance-movement-token-tips-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Movement Token Tips" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Let fans tip dancers instantly with tokens and no gas fees. Discipline: Dance & Choreography (fan support). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet and sponsored tx remove barriers to microtransactions. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MovementTokenTips.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogMovementTokenTips /// @notice Let fans tip dancers instantly with tokens and no gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogMovementTokenTips { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MovementTokenTips` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MovementTokenTips.sol MovementTokenTips` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every fan support action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-movemint-studio-20 · dance · MoveMint Studio ===== Pitch: Choreographers mint unique dance moves as NFTs to monetize and protect their creations. Page: https://arcprompts.lovable.app/ideas/dance-movemint-studio-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MoveMint Studio" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Choreographers mint unique dance moves as NFTs to monetize and protect their creations. Discipline: Dance & Choreography (original move minting). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 minting enables secure creator ownership and provenance verification. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MoveMintStudio.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MoveMintStudio /// @notice ERC-721 provenance for: Choreographers mint unique dance moves as NFTs to monetize and protect their creations. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MoveMintStudio is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MoveMintStudio", "MOVEMI") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MoveMintStudio` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MoveMintStudio.sol MoveMintStudio` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a original move minting, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-movemint-workshops-15 · dance · MoveMint Workshops ===== Pitch: Mint limited access tokens to exclusive dance workshops and masterclasses as NFTs. Page: https://arcprompts.lovable.app/ideas/dance-movemint-workshops-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MoveMint Workshops" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint limited access tokens to exclusive dance workshops and masterclasses as NFTs. Discipline: Dance & Choreography (tokenized education). Onchain primitive: Arc smart contract. Why this primitive: Onchain NFT tickets guarantee authenticity and scarcity for premium educational content. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MoveMintWorkshops.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title MoveMintWorkshops /// @notice Mint limited access tokens to exclusive dance workshops and masterclasses as NFTs. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MoveMintWorkshops { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MoveMintWorkshops` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MoveMintWorkshops.sol MoveMintWorkshops` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a tokenized education action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-moveproof-ledger-5 · dance · MoveProof Ledger ===== Pitch: Record and verify unique dance moves proof-of-existence onchain to counter plagiarism. Page: https://arcprompts.lovable.app/ideas/dance-moveproof-ledger-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MoveProof Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record and verify unique dance moves proof-of-existence onchain to counter plagiarism. Discipline: Dance & Choreography (movement authenticity). Onchain primitive: Arc smart contract. Why this primitive: Immutable smart contracts provide tamper-proof timestamps for movement intellectual property. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MoveProofLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title MoveProofLedger /// @notice Record and verify unique dance moves proof-of-existence onchain to counter plagiarism. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MoveProofLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MoveProofLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MoveProofLedger.sol MoveProofLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a movement authenticity action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-nft-dance-collectives-24 · dance · NFT Dance Collectives ===== Pitch: Form dance groups managing collective NFTs and revenue with gasless transactions. Page: https://arcprompts.lovable.app/ideas/dance-nft-dance-collectives-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NFT Dance Collectives" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Form dance groups managing collective NFTs and revenue with gasless transactions. Discipline: Dance & Choreography (group ownership). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social and sponsored tx enable easy group wallet management. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NFTDanceCollectives.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogNFTDanceCollectives /// @notice Form dance groups managing collective NFTs and revenue with gasless transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogNFTDanceCollectives { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NFTDanceCollectives` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NFTDanceCollectives.sol NFTDanceCollectives` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every group ownership action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-nft-move-badges-8 · dance · NFT Move Badges ===== Pitch: Issue unique NFT badges for dance milestones and skills to motivate learners. Page: https://arcprompts.lovable.app/ideas/dance-nft-move-badges-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NFT Move Badges" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Issue unique NFT badges for dance milestones and skills to motivate learners. Discipline: Dance & Choreography (achievement tracking). Onchain primitive: Arc smart contract. Why this primitive: Onchain issuance guarantees badge uniqueness and verifiability worldwide. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NFTMoveBadges.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title NFTMoveBadges /// @notice Issue unique NFT badges for dance milestones and skills to motivate learners. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract NFTMoveBadges { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NFTMoveBadges` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NFTMoveBadges.sol NFTMoveBadges` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a achievement tracking action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-performance-ticket-nfts-18 · dance · Performance Ticket NFTs ===== Pitch: Issue and verify dance performance tickets as NFTs with no gas costs for attendees. Page: https://arcprompts.lovable.app/ideas/dance-performance-ticket-nfts-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Performance Ticket NFTs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Issue and verify dance performance tickets as NFTs with no gas costs for attendees. Discipline: Dance & Choreography (ticketing and access). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet and sponsored tx simplify ticket ownership experience. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PerformanceTicketNFTs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPerformanceTicketNFTs /// @notice Issue and verify dance performance tickets as NFTs with no gas costs for attendees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPerformanceTicketNFTs { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PerformanceTicketNFTs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PerformanceTicketNFTs.sol PerformanceTicketNFTs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every ticketing and access action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-posechain-gallery-21 · dance · PoseChain Gallery ===== Pitch: Create galleries of NFT-verified dance poses to showcase and sell digital art collections. Page: https://arcprompts.lovable.app/ideas/dance-posechain-gallery-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PoseChain Gallery" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create galleries of NFT-verified dance poses to showcase and sell digital art collections. Discipline: Dance & Choreography (dance pose exhibitions). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance ensures uniqueness and rightful ownership onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PoseChainGallery.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PoseChainGallery /// @notice ERC-721 provenance for: Create galleries of NFT-verified dance poses to showcase and sell digital art collections. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PoseChainGallery is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PoseChainGallery", "POSECH") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PoseChainGallery` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PoseChainGallery.sol PoseChainGallery` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a dance pose exhibitions, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-posemint-studio-3 · dance · PoseMint Studio ===== Pitch: Create NFT collections of signature dance poses to license and showcase unique styles. Page: https://arcprompts.lovable.app/ideas/dance-posemint-studio-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PoseMint Studio" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create NFT collections of signature dance poses to license and showcase unique styles. Discipline: Dance & Choreography (pose collections). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT minting guarantees original pose ownership and provenance onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PoseMintStudio.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PoseMintStudio /// @notice ERC-721 provenance for: Create NFT collections of signature dance poses to license and showcase unique styles. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PoseMintStudio is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PoseMintStudio", "POSEMI") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PoseMintStudio` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PoseMintStudio.sol PoseMintStudio` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a pose collections, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-pulseproof-archives-12 · dance · PulseProof Archives ===== Pitch: Create immutable NFT records of historical dance footage for rights and preservation. Page: https://arcprompts.lovable.app/ideas/dance-pulseproof-archives-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PulseProof Archives" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create immutable NFT records of historical dance footage for rights and preservation. Discipline: Dance & Choreography (historical dance archives). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs anchor archival IPFS content with verifiable creator provenance onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PulseProofArchives.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PulseProofArchives /// @notice ERC-721 provenance for: Create immutable NFT records of historical dance footage for rights and preservation. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PulseProofArchives is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PulseProofArchives", "PULSEP") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PulseProofArchives` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PulseProofArchives.sol PulseProofArchives` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a historical dance archives, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-rhythmripple-chain-19 · dance · RhythmRipple Chain ===== Pitch: Tokenize rhythm sequences as NFTs for secure ownership and licensing in dance shows. Page: https://arcprompts.lovable.app/ideas/dance-rhythmripple-chain-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "RhythmRipple Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Tokenize rhythm sequences as NFTs for secure ownership and licensing in dance shows. Discipline: Dance & Choreography (beat choreography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance ensures trusted beat IP management and transfer. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/RhythmRippleChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title RhythmRippleChain /// @notice ERC-721 provenance for: Tokenize rhythm sequences as NFTs for secure ownership and licensing in dance shows. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract RhythmRippleChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("RhythmRippleChain", "RHYTHM") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs RhythmRippleChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/RhythmRippleChain.sol RhythmRippleChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a beat choreography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-rhythmroots-nft-6 · dance · RhythmRoots NFT ===== Pitch: Preserve cultural dances as NFT-verified IPFS multimedia packages for global education. Page: https://arcprompts.lovable.app/ideas/dance-rhythmroots-nft-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "RhythmRoots NFT" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Preserve cultural dances as NFT-verified IPFS multimedia packages for global education. Discipline: Dance & Choreography (cultural dance preservation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance protects cultural IP and ensures authenticity and respect. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/RhythmRootsNFT.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title RhythmRootsNFT /// @notice ERC-721 provenance for: Preserve cultural dances as NFT-verified IPFS multimedia packages for global education. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract RhythmRootsNFT is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("RhythmRootsNFT", "RHYTHM") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs RhythmRootsNFT` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/RhythmRootsNFT.sol RhythmRootsNFT` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a cultural dance preservation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-shadowstep-registry-23 · dance · ShadowStep Registry ===== Pitch: Catalog silent or lip-synced dance movements as NFTs for licensing and crediting. Page: https://arcprompts.lovable.app/ideas/dance-shadowstep-registry-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ShadowStep Registry" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Catalog silent or lip-synced dance movements as NFTs for licensing and crediting. Discipline: Dance & Choreography (silent movement catalog). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance provides clear IP proof for silent choreography assets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ShadowStepRegistry.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ShadowStepRegistry /// @notice ERC-721 provenance for: Catalog silent or lip-synced dance movements as NFTs for licensing and crediting. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ShadowStepRegistry is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ShadowStepRegistry", "SHADOW") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ShadowStepRegistry` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ShadowStepRegistry.sol ShadowStepRegistry` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a silent movement catalog, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-spinmint-marketplace-13 · dance · SpinMint Marketplace ===== Pitch: Tokenize 360° recorded dance moves as NFTs to share with immersive digital experiences. Page: https://arcprompts.lovable.app/ideas/dance-spinmint-marketplace-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SpinMint Marketplace" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Tokenize 360° recorded dance moves as NFTs to share with immersive digital experiences. Discipline: Dance & Choreography (360 dance moves). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs guarantee ownership of complex multimedia dance assets stored on IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SpinMintMarketplace.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SpinMintMarketplace /// @notice ERC-721 provenance for: Tokenize 360° recorded dance moves as NFTs to share with immersive digital experiences. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SpinMintMarketplace is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SpinMintMarketplace", "SPINMI") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SpinMintMarketplace` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SpinMintMarketplace.sol SpinMintMarketplace` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a 360 dance moves, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-stepchain-voting-16 · dance · StepChain Voting ===== Pitch: Let audiences vote transparently and fairly on dance competition outcomes via smart contracts. Page: https://arcprompts.lovable.app/ideas/dance-stepchain-voting-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StepChain Voting" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Let audiences vote transparently and fairly on dance competition outcomes via smart contracts. Discipline: Dance & Choreography (event curation). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide immutable, verifiable vote tallying without manipulation. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StepChainVoting.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title StepChainVoting /// @notice Let audiences vote transparently and fairly on dance competition outcomes via smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StepChainVoting { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StepChainVoting` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StepChainVoting.sol StepChainVoting` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a event curation action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-stepswap-royalties-19 · dance · StepSwap Royalties ===== Pitch: Track and enforce royalty payments on resales of choreography NFTs via smart contracts. Page: https://arcprompts.lovable.app/ideas/dance-stepswap-royalties-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StepSwap Royalties" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track and enforce royalty payments on resales of choreography NFTs via smart contracts. Discipline: Dance & Choreography (resale tracking). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts automatically route royalties ensuring creator revenue continuity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StepSwapRoyalties.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title StepSwapRoyalties /// @notice Track and enforce royalty payments on resales of choreography NFTs via smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StepSwapRoyalties { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StepSwapRoyalties` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StepSwapRoyalties.sol StepSwapRoyalties` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a resale tracking action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-stepsync-collectibles-9 · dance · StepSync Collectibles ===== Pitch: Create collectible NFTs of iconic dance steps for fans and teaching accreditation. Page: https://arcprompts.lovable.app/ideas/dance-stepsync-collectibles-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StepSync Collectibles" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create collectible NFTs of iconic dance steps for fans and teaching accreditation. Discipline: Dance & Choreography (signature step NFTs). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Immutable NFTs certify ownership and authenticity of iconic dance moves. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StepSyncCollectibles.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title StepSyncCollectibles /// @notice ERC-721 provenance for: Create collectible NFTs of iconic dance steps for fans and teaching accreditation. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StepSyncCollectibles is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("StepSyncCollectibles", "STEPSY") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StepSyncCollectibles` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StepSyncCollectibles.sol StepSyncCollectibles` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a signature step NFTs, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-stepsync-remix-16 · dance · StepSync Remix ===== Pitch: Mint and license remixable dance steps as NFTs to promote collaborative creation. Page: https://arcprompts.lovable.app/ideas/dance-stepsync-remix-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StepSync Remix" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint and license remixable dance steps as NFTs to promote collaborative creation. Discipline: Dance & Choreography (dance step sampling). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs track sample provenance and usage rights transparently onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StepSyncRemix.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title StepSyncRemix /// @notice ERC-721 provenance for: Mint and license remixable dance steps as NFTs to promote collaborative creation. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StepSyncRemix is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("StepSyncRemix", "STEPSY") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StepSyncRemix` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StepSyncRemix.sol StepSyncRemix` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a dance step sampling, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-steptrace-rights-4 · dance · StepTrace Rights ===== Pitch: Mint entire step sequences as NFTs to secure choreographic rights and facilitate licensing. Page: https://arcprompts.lovable.app/ideas/dance-steptrace-rights-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StepTrace Rights" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint entire step sequences as NFTs to secure choreographic rights and facilitate licensing. Discipline: Dance & Choreography (step sequence rights). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs provide immutable records of step sequences with creator proofs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StepTraceRights.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title StepTraceRights /// @notice ERC-721 provenance for: Mint entire step sequences as NFTs to secure choreographic rights and facilitate licensing. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StepTraceRights is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("StepTraceRights", "STEPTR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StepTraceRights` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StepTraceRights.sol StepTraceRights` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a step sequence rights, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-studio-access-club-2 · dance · Studio Access Club ===== Pitch: Create token-gated memberships for dance studios with seamless gas-free onboarding. Page: https://arcprompts.lovable.app/ideas/dance-studio-access-club-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Studio Access Club" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create token-gated memberships for dance studios with seamless gas-free onboarding. Discipline: Dance & Choreography (membership management). Onchain primitive: Privy social wallet on Arc. Why this primitive: Embedded wallet and sponsored transactions simplify user entry and retention. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StudioAccessClub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogStudioAccessClub /// @notice Create token-gated memberships for dance studios with seamless gas-free onboarding. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogStudioAccessClub { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StudioAccessClub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StudioAccessClub.sol StudioAccessClub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every membership management action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-syncstage-auction-4 · dance · SyncStage Auction ===== Pitch: Auction live dance performance slots to global choreographers via transparent smart contracts. Page: https://arcprompts.lovable.app/ideas/dance-syncstage-auction-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SyncStage Auction" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Auction live dance performance slots to global choreographers via transparent smart contracts. Discipline: Dance & Choreography (live collaboration). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts enable trustless, automated auction and booking processes worldwide. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SyncStageAuction.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SyncStageAuction /// @notice Auction live dance performance slots to global choreographers via transparent smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SyncStageAuction { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SyncStageAuction` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SyncStageAuction.sol SyncStageAuction` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a live collaboration action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-technique-archive-hub-4 · dance · Technique Archive Hub ===== Pitch: Pin and share detailed dance technique manuals and videos as permanent IPFS content. Page: https://arcprompts.lovable.app/ideas/dance-technique-archive-hub-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Technique Archive Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin and share detailed dance technique manuals and videos as permanent IPFS content. Discipline: Dance & Choreography (dance training materials). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS provides a censorship-resistant platform for long-term educational content storage. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TechniqueArchiveHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentTechniqueArchiveHub /// @notice Pin and share detailed dance technique manuals and videos as permanent IPFS content. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentTechniqueArchiveHub { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TechniqueArchiveHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TechniqueArchiveHub.sol TechniqueArchiveHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a dance training materials, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-tokenized-choreo-kits-6 · dance · Tokenized Choreo Kits ===== Pitch: Sell bundled choreography sequences as tokenized assets for easier licensing and resale. Page: https://arcprompts.lovable.app/ideas/dance-tokenized-choreo-kits-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Tokenized Choreo Kits" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Sell bundled choreography sequences as tokenized assets for easier licensing and resale. Discipline: Dance & Choreography (content packaging). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts facilitate fractional ownership and transparent transfers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TokenizedChoreoKits.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title TokenizedChoreoKits /// @notice Sell bundled choreography sequences as tokenized assets for easier licensing and resale. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TokenizedChoreoKits { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TokenizedChoreoKits` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TokenizedChoreoKits.sol TokenizedChoreoKits` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a content packaging action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-tokenmove-sponsorship-22 · dance · TokenMove Sponsorship ===== Pitch: Enable brands to sponsor dancers directly through programmable tokens and onchain contracts. Page: https://arcprompts.lovable.app/ideas/dance-tokenmove-sponsorship-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "TokenMove Sponsorship" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable brands to sponsor dancers directly through programmable tokens and onchain contracts. Discipline: Dance & Choreography (brand engagement). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts transparently automate sponsor payments and benefits with auditability. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TokenMoveSponsorship.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title TokenMoveSponsorship /// @notice Enable brands to sponsor dancers directly through programmable tokens and onchain contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TokenMoveSponsorship { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TokenMoveSponsorship` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TokenMoveSponsorship.sol TokenMoveSponsorship` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a brand engagement action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-virtual-dance-studios-17 · dance · Virtual Dance Studios ===== Pitch: Access and customize virtual dance studios with gasless token gating. Page: https://arcprompts.lovable.app/ideas/dance-virtual-dance-studios-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Virtual Dance Studios" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Access and customize virtual dance studios with gasless token gating. Discipline: Dance & Choreography (metaverse dance spaces). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social wallet and sponsored tx make metaverse entry seamless. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VirtualDanceStudios.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogVirtualDanceStudios /// @notice Access and customize virtual dance studios with gasless token gating. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogVirtualDanceStudios { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VirtualDanceStudios` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VirtualDanceStudios.sol VirtualDanceStudios` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every metaverse dance spaces action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA dance-virtual-rehearsal-logs-9 · dance · Virtual Rehearsal Logs ===== Pitch: Pin JSON logs of rehearsal notes and images on IPFS for permanent, shareable practice records. Page: https://arcprompts.lovable.app/ideas/dance-virtual-rehearsal-logs-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Virtual Rehearsal Logs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin JSON logs of rehearsal notes and images on IPFS for permanent, shareable practice records. Discipline: Dance & Choreography (practice session tracking). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS ensures immutable, censorship-resistant rehearsal documentation accessible anytime. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VirtualRehearsalLogs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentVirtualRehearsalLogs /// @notice Pin JSON logs of rehearsal notes and images on IPFS for permanent, shareable practice records. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentVirtualRehearsalLogs { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VirtualRehearsalLogs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VirtualRehearsalLogs.sol VirtualRehearsalLogs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a practice session tracking, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-accessory-archive-8 · fashion · Accessory Archive ===== Pitch: Pin detailed accessory designs for easy access and IP validation. Page: https://arcprompts.lovable.app/ideas/fashion-accessory-archive-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Accessory Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin detailed accessory designs for easy access and IP validation. Discipline: Fashion & Textile Design (digital accessory cataloging). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata enables permanent, tamper-resistant archival of accessory assets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AccessoryArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAccessoryArchive /// @notice Pin detailed accessory designs for easy access and IP validation. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAccessoryArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AccessoryArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AccessoryArchive.sol AccessoryArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a digital accessory cataloging, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-accessory-archives-12 · fashion · Accessory Archives ===== Pitch: Digitally mint exclusive NFT records of original jewelry designs with provenance. Page: https://arcprompts.lovable.app/ideas/fashion-accessory-archives-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Accessory Archives" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Digitally mint exclusive NFT records of original jewelry designs with provenance. Discipline: Fashion & Textile Design (jewelry design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 on Sepolia links unique design CIDs to verified creators immutably. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AccessoryArchives.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title AccessoryArchives /// @notice ERC-721 provenance for: Digitally mint exclusive NFT records of original jewelry designs with provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AccessoryArchives is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("AccessoryArchives", "ACCESS") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AccessoryArchives` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AccessoryArchives.sol AccessoryArchives` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a jewelry design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-accessory-auth-10 · fashion · Accessory Auth ===== Pitch: Provide verified origin records for fashion accessories and fine jewelry owners. Page: https://arcprompts.lovable.app/ideas/fashion-accessory-auth-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Accessory Auth" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Provide verified origin records for fashion accessories and fine jewelry owners. Discipline: Fashion & Textile Design (jewelry provenance). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social login and sponsored tx enable secure provenance recording and easy user access. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AccessoryAuth.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogAccessoryAuth /// @notice Provide verified origin records for fashion accessories and fine jewelry owners. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogAccessoryAuth { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AccessoryAuth` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AccessoryAuth.sol AccessoryAuth` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every jewelry provenance action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-accessory-authenticator-20 · fashion · Accessory Authenticator ===== Pitch: Mint unique NFTs proving authenticity and limited runs of accessory designs. Page: https://arcprompts.lovable.app/ideas/fashion-accessory-authenticator-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Accessory Authenticator" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint unique NFTs proving authenticity and limited runs of accessory designs. Discipline: Fashion & Textile Design (limited-edition accessories). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens guarantee verifiable scarcity and provenance onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AccessoryAuthenticator.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title AccessoryAuthenticator /// @notice ERC-721 provenance for: Mint unique NFTs proving authenticity and limited runs of accessory designs. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AccessoryAuthenticator is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("AccessoryAuthenticator", "ACCESS") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AccessoryAuthenticator` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AccessoryAuthenticator.sol AccessoryAuthenticator` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a limited-edition accessories, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-accessoryauth-mint-19 · fashion · AccessoryAuth Mint ===== Pitch: Issue NFTs authenticating limited-edition fashion accessories to combat counterfeits. Page: https://arcprompts.lovable.app/ideas/fashion-accessoryauth-mint-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AccessoryAuth Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Issue NFTs authenticating limited-edition fashion accessories to combat counterfeits. Discipline: Fashion & Textile Design (accessory authentication). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts provide verifiable ownership and authenticity certificates. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AccessoryAuthMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AccessoryAuthMint /// @notice Issue NFTs authenticating limited-edition fashion accessories to combat counterfeits. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AccessoryAuthMint { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AccessoryAuthMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AccessoryAuthMint.sol AccessoryAuthMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a accessory authentication action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-avant-atlas-14 · fashion · Avant Atlas ===== Pitch: Store experimental fashion concepts securely to document avant-garde evolution. Page: https://arcprompts.lovable.app/ideas/fashion-avant-atlas-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Avant Atlas" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store experimental fashion concepts securely to document avant-garde evolution. Discipline: Fashion & Textile Design (experimental design archives). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata's permanent storage preserves niche design experiments transparently. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AvantAtlas.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAvantAtlas /// @notice Store experimental fashion concepts securely to document avant-garde evolution. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAvantAtlas { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AvantAtlas` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AvantAtlas.sol AvantAtlas` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a experimental design archives, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-biofiber-token-10 · fashion · BioFiber Token ===== Pitch: Tokenize biomaterial innovation projects with secure on-chain investment tracking. Page: https://arcprompts.lovable.app/ideas/fashion-biofiber-token-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "BioFiber Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Tokenize biomaterial innovation projects with secure on-chain investment tracking. Discipline: Fashion & Textile Design (biomaterial crowdfunding). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts distribute investment tokens and handle transparent fund flows. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/BioFiberToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title BioFiberToken /// @notice Tokenize biomaterial innovation projects with secure on-chain investment tracking. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract BioFiberToken { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs BioFiberToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/BioFiberToken.sol BioFiberToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a biomaterial crowdfunding action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-capsule-catalog-9 · fashion · Capsule Catalog ===== Pitch: Store entire seasonal lookbooks as immutable IPFS manifests for authenticity. Page: https://arcprompts.lovable.app/ideas/fashion-capsule-catalog-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Capsule Catalog" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store entire seasonal lookbooks as immutable IPFS manifests for authenticity. Discipline: Fashion & Textile Design (seasonal collection storage). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata ensures permanent, verifiable storage of complex collection data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CapsuleCatalog.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentCapsuleCatalog /// @notice Store entire seasonal lookbooks as immutable IPFS manifests for authenticity. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentCapsuleCatalog { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CapsuleCatalog` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CapsuleCatalog.sol CapsuleCatalog` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a seasonal collection storage, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-capsule-claim-24 · fashion · Capsule Claim ===== Pitch: Issue onchain certificates for limited edition capsule collections ensuring buyer confidence. Page: https://arcprompts.lovable.app/ideas/fashion-capsule-claim-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Capsule Claim" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Issue onchain certificates for limited edition capsule collections ensuring buyer confidence. Discipline: Fashion & Textile Design (limited drop authentication). Onchain primitive: Privy social wallet on Arc. Why this primitive: Google sign-in with sponsored tx allows secure certificate minting without gas hurdles. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CapsuleClaim.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogCapsuleClaim /// @notice Issue onchain certificates for limited edition capsule collections ensuring buyer confidence. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogCapsuleClaim { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CapsuleClaim` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CapsuleClaim.sol CapsuleClaim` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every limited drop authentication action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-capsule-contract-11 · fashion · Capsule Contract ===== Pitch: Enable designers to co-create and manage capsule collections with frictionless onchain agreements. Page: https://arcprompts.lovable.app/ideas/fashion-capsule-contract-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Capsule Contract" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable designers to co-create and manage capsule collections with frictionless onchain agreements. Discipline: Fashion & Textile Design (collaborative capsule collections). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored tx simplifies contract setup and management without gas costs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CapsuleContract.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogCapsuleContract /// @notice Enable designers to co-create and manage capsule collections with frictionless onchain agreements. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogCapsuleContract { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CapsuleContract` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CapsuleContract.sol CapsuleContract` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every collaborative capsule collections action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-capsule-mint-7 · fashion · Capsule Mint ===== Pitch: Issue limited-edition fashion collection NFTs with provenance for pre-sale exclusivity. Page: https://arcprompts.lovable.app/ideas/fashion-capsule-mint-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Capsule Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Issue limited-edition fashion collection NFTs with provenance for pre-sale exclusivity. Discipline: Fashion & Textile Design (collection drops). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens provide verifiable scarcity and authenticity for digital fashion drops. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CapsuleMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title CapsuleMint /// @notice ERC-721 provenance for: Issue limited-edition fashion collection NFTs with provenance for pre-sale exclusivity. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CapsuleMint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("CapsuleMint", "CAPSUL") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CapsuleMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CapsuleMint.sol CapsuleMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a collection drops, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-color-code-vault-2 · fashion · Color Code Vault ===== Pitch: Store confirmed color palettes and their usage securely for design authenticity. Page: https://arcprompts.lovable.app/ideas/fashion-color-code-vault-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Color Code Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store confirmed color palettes and their usage securely for design authenticity. Discipline: Fashion & Textile Design (color palette preservation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata's immutable storage validates and preserves exact color codes over time. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ColorCodeVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentColorCodeVault /// @notice Store confirmed color palettes and their usage securely for design authenticity. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentColorCodeVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ColorCodeVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ColorCodeVault.sol ColorCodeVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a color palette preservation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-colorchain-palette-4 · fashion · ColorChain Palette ===== Pitch: Verify and share color formula origins to protect designer intellectual property. Page: https://arcprompts.lovable.app/ideas/fashion-colorchain-palette-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ColorChain Palette" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Verify and share color formula origins to protect designer intellectual property. Discipline: Fashion & Textile Design (color provenance). Onchain primitive: Privy social wallet on Arc. Why this primitive: Google sign-in and gasless transactions simplify tracking color data ownership onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ColorChainPalette.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogColorChainPalette /// @notice Verify and share color formula origins to protect designer intellectual property. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogColorChainPalette { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ColorChainPalette` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ColorChainPalette.sol ColorChainPalette` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every color provenance action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-colorcode-ledger-4 · fashion · ColorCode Ledger ===== Pitch: Authenticate unique color palettes as exclusive NFTs for designers' intellectual property. Page: https://arcprompts.lovable.app/ideas/fashion-colorcode-ledger-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ColorCode Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate unique color palettes as exclusive NFTs for designers' intellectual property. Discipline: Fashion & Textile Design (color study). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain minting secures color palette uniqueness and ownership transparently. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ColorCodeLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ColorCodeLedger /// @notice ERC-721 provenance for: Authenticate unique color palettes as exclusive NFTs for designers' intellectual property. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ColorCodeLedger is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ColorCodeLedger", "COLORC") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ColorCodeLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ColorCodeLedger.sol ColorCodeLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a color study, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-colorpalette-dao-2 · fashion · ColorPalette DAO ===== Pitch: Collaboratively create and share color palettes with verified ownership and royalty rules. Page: https://arcprompts.lovable.app/ideas/fashion-colorpalette-dao-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ColorPalette DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Collaboratively create and share color palettes with verified ownership and royalty rules. Discipline: Fashion & Textile Design (color curation). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enable decentralized governance and royalty distribution. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ColorPaletteDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ColorPaletteDAO /// @notice Collaboratively create and share color palettes with verified ownership and royalty rules. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ColorPaletteDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ColorPaletteDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ColorPaletteDAO.sol ColorPaletteDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a color curation action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-costume-chronicle-1 · fashion · Costume Chronicle ===== Pitch: Securely mint and showcase original costume designs as unique digital collectibles. Page: https://arcprompts.lovable.app/ideas/fashion-costume-chronicle-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Costume Chronicle" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely mint and showcase original costume designs as unique digital collectibles. Discipline: Fashion & Textile Design (historical costume design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens uniquely represent each costume's verified digital identity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CostumeChronicle.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title CostumeChronicle /// @notice ERC-721 provenance for: Securely mint and showcase original costume designs as unique digital collectibles. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CostumeChronicle is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("CostumeChronicle", "COSTUM") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CostumeChronicle` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CostumeChronicle.sol CostumeChronicle` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a historical costume design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-costume-chronicle-17 · fashion · Costume Chronicle ===== Pitch: Securely archive costume design images and details for production history. Page: https://arcprompts.lovable.app/ideas/fashion-costume-chronicle-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Costume Chronicle" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely archive costume design images and details for production history. Discipline: Fashion & Textile Design (theatrical costume records). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata pins ensure durable, tamper-proof costume archives. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CostumeChronicle.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentCostumeChronicle /// @notice Securely archive costume design images and details for production history. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentCostumeChronicle { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CostumeChronicle` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CostumeChronicle.sol CostumeChronicle` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a theatrical costume records, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-costumenft-archive-7 · fashion · CostumeNFT Archive ===== Pitch: Create verified digital archives of historical costumes as NFTs for museums and artists. Page: https://arcprompts.lovable.app/ideas/fashion-costumenft-archive-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "CostumeNFT Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create verified digital archives of historical costumes as NFTs for museums and artists. Discipline: Fashion & Textile Design (historical costume catalog). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts preserve provenance and authenticity for cultural assets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CostumeNFTArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title CostumeNFTArchive /// @notice Create verified digital archives of historical costumes as NFTs for museums and artists. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CostumeNFTArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CostumeNFTArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CostumeNFTArchive.sol CostumeNFTArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a historical costume catalog action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-craft-chain-connect-21 · fashion · Craft Chain Connect ===== Pitch: Mint collaborative NFTs documenting co-creation between designers and artisans. Page: https://arcprompts.lovable.app/ideas/fashion-craft-chain-connect-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Craft Chain Connect" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint collaborative NFTs documenting co-creation between designers and artisans. Discipline: Fashion & Textile Design (artisan collaboration). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs record joint provenance and IP ownership on immutable blockchain records. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CraftChainConnect.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title CraftChainConnect /// @notice ERC-721 provenance for: Mint collaborative NFTs documenting co-creation between designers and artisans. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CraftChainConnect is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("CraftChainConnect", "CRAFTC") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CraftChainConnect` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CraftChainConnect.sol CraftChainConnect` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a artisan collaboration, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-craft-credit-16 · fashion · Craft Credit ===== Pitch: Automatically credit textile artisans in design chains with transparent onchain records. Page: https://arcprompts.lovable.app/ideas/fashion-craft-credit-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Craft Credit" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Automatically credit textile artisans in design chains with transparent onchain records. Discipline: Fashion & Textile Design (artisan attribution). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet and sponsored tx automate attribution without user gas concerns. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CraftCredit.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogCraftCredit /// @notice Automatically credit textile artisans in design chains with transparent onchain records. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogCraftCredit { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CraftCredit` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CraftCredit.sol CraftCredit` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every artisan attribution action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-design-chain-diary-6 · fashion · Design Chain Diary ===== Pitch: Chronologically mint fashion sketches as NFTs to prove creative progress and ownership. Page: https://arcprompts.lovable.app/ideas/fashion-design-chain-diary-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Design Chain Diary" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Chronologically mint fashion sketches as NFTs to prove creative progress and ownership. Discipline: Fashion & Textile Design (fashion sketching). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Immutable onchain records timestamp and protect design evolution securely. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DesignChainDiary.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title DesignChainDiary /// @notice ERC-721 provenance for: Chronologically mint fashion sketches as NFTs to prove creative progress and ownership. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DesignChainDiary is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("DesignChainDiary", "DESIGN") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DesignChainDiary` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DesignChainDiary.sol DesignChainDiary` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a fashion sketching, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-designauth-mint-1 · fashion · DesignAuth Mint ===== Pitch: Securely register and timestamp original fashion designs to prevent copying. Page: https://arcprompts.lovable.app/ideas/fashion-designauth-mint-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "DesignAuth Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely register and timestamp original fashion designs to prevent copying. Discipline: Fashion & Textile Design (design copyright). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts provide decentralized, tamper-proof timestamps for design IP. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DesignAuthMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DesignAuthMint /// @notice Securely register and timestamp original fashion designs to prevent copying. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DesignAuthMint { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DesignAuthMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DesignAuthMint.sol DesignAuthMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a design copyright action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-designer-direct-23 · fashion · Designer Direct ===== Pitch: Connect designers directly with suppliers using onchain verified transactions and profiles. Page: https://arcprompts.lovable.app/ideas/fashion-designer-direct-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Designer Direct" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Connect designers directly with suppliers using onchain verified transactions and profiles. Discipline: Fashion & Textile Design (supply chain transparency). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social login and gasless tx simplify supply chain verification and reduce intermediaries. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DesignerDirect.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogDesignerDirect /// @notice Connect designers directly with suppliers using onchain verified transactions and profiles. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogDesignerDirect { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DesignerDirect` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DesignerDirect.sol DesignerDirect` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every supply chain transparency action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-digital-drapery-hub-22 · fashion · Digital Drapery Hub ===== Pitch: Pin fabric simulation data and images for shared digital draping experiences. Page: https://arcprompts.lovable.app/ideas/fashion-digital-drapery-hub-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Digital Drapery Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin fabric simulation data and images for shared digital draping experiences. Discipline: Fashion & Textile Design (online fabric simulation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS pins preserve large simulation files with permanent access. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DigitalDraperyHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentDigitalDraperyHub /// @notice Pin fabric simulation data and images for shared digital draping experiences. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentDigitalDraperyHub { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DigitalDraperyHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DigitalDraperyHub.sol DigitalDraperyHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a online fabric simulation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-digital-drapes-18 · fashion · Digital Drapes ===== Pitch: Create NFTs certifying original digital drape and garment simulations for designers. Page: https://arcprompts.lovable.app/ideas/fashion-digital-drapes-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Digital Drapes" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create NFTs certifying original digital drape and garment simulations for designers. Discipline: Fashion & Textile Design (3D garment design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens link 3D simulation files to verified creators on IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DigitalDrapes.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title DigitalDrapes /// @notice ERC-721 provenance for: Create NFTs certifying original digital drape and garment simulations for designers. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DigitalDrapes is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("DigitalDrapes", "DIGITA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DigitalDrapes` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DigitalDrapes.sol DigitalDrapes` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a 3D garment design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-digitalswatch-token-21 · fashion · DigitalSwatch Token ===== Pitch: Create and trade verified digital fabric swatches with immutable ownership records. Page: https://arcprompts.lovable.app/ideas/fashion-digitalswatch-token-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "DigitalSwatch Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create and trade verified digital fabric swatches with immutable ownership records. Discipline: Fashion & Textile Design (digital fabric samples). Onchain primitive: Arc smart contract. Why this primitive: Onchain tokens assure sample authenticity and provenance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DigitalSwatchToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DigitalSwatchToken /// @notice Create and trade verified digital fabric swatches with immutable ownership records. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DigitalSwatchToken { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DigitalSwatchToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DigitalSwatchToken.sol DigitalSwatchToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a digital fabric samples action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-drape-documentation-10 · fashion · Drape Documentation ===== Pitch: Pin high-res photos and notes on draping experiments for design validation. Page: https://arcprompts.lovable.app/ideas/fashion-drape-documentation-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Drape Documentation" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin high-res photos and notes on draping experiments for design validation. Discipline: Fashion & Textile Design (fabric draping records). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS pins preserve ephemeral draping visuals immutably and accessibly. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DrapeDocumentation.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentDrapeDocumentation /// @notice Pin high-res photos and notes on draping experiments for design validation. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentDrapeDocumentation { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DrapeDocumentation` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DrapeDocumentation.sol DrapeDocumentation` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a fabric draping records, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-ecofabric-ledger-12 · fashion · EcoFabric Ledger ===== Pitch: Archive sustainability data and certifications for eco-friendly fabrics. Page: https://arcprompts.lovable.app/ideas/fashion-ecofabric-ledger-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "EcoFabric Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Archive sustainability data and certifications for eco-friendly fabrics. Discipline: Fashion & Textile Design (sustainable textile tracking). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata ensures permanent, verifiable sustainability information storage. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EcoFabricLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentEcoFabricLedger /// @notice Archive sustainability data and certifications for eco-friendly fabrics. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentEcoFabricLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EcoFabricLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EcoFabricLedger.sol EcoFabricLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a sustainable textile tracking, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-ecofashion-dao-22 · fashion · EcoFashion DAO ===== Pitch: Govern eco-friendly fashion initiatives via decentralized, transparent smart contracts. Page: https://arcprompts.lovable.app/ideas/fashion-ecofashion-dao-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "EcoFashion DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Govern eco-friendly fashion initiatives via decentralized, transparent smart contracts. Discipline: Fashion & Textile Design (sustainability governance). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enable democratic decision-making and fund allocation. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EcoFashionDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title EcoFashionDAO /// @notice Govern eco-friendly fashion initiatives via decentralized, transparent smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract EcoFashionDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EcoFashionDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EcoFashionDAO.sol EcoFashionDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a sustainability governance action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-ethical-edit-log-23 · fashion · Ethical Edit Log ===== Pitch: Pin timestamps and notes on ethical design choices for industry trust. Page: https://arcprompts.lovable.app/ideas/fashion-ethical-edit-log-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Ethical Edit Log" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin timestamps and notes on ethical design choices for industry trust. Discipline: Fashion & Textile Design (design decision auditing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata provides immutable proof of ethical design documentation. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EthicalEditLog.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentEthicalEditLog /// @notice Pin timestamps and notes on ethical design choices for industry trust. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentEthicalEditLog { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EthicalEditLog` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EthicalEditLog.sol EthicalEditLog` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a design decision auditing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-ethnic-essence-19 · fashion · Ethnic Essence ===== Pitch: Mint NFTs to protect and showcase indigenous and ethnic textile designs. Page: https://arcprompts.lovable.app/ideas/fashion-ethnic-essence-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Ethnic Essence" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs to protect and showcase indigenous and ethnic textile designs. Discipline: Fashion & Textile Design (cultural textiles). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance mints preserve cultural IP and provenance securely and transparently. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EthnicEssence.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title EthnicEssence /// @notice ERC-721 provenance for: Mint NFTs to protect and showcase indigenous and ethnic textile designs. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract EthnicEssence is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("EthnicEssence", "ETHNIC") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EthnicEssence` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EthnicEssence.sol EthnicEssence` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a cultural textiles, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-fabric-fraction-8 · fashion · Fabric Fraction ===== Pitch: Enable fractional ownership of rare textile swatches through secure tokenization and seamless onboarding. Page: https://arcprompts.lovable.app/ideas/fashion-fabric-fraction-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Fabric Fraction" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable fractional ownership of rare textile swatches through secure tokenization and seamless onboarding. Discipline: Fashion & Textile Design (material micro-ownership). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet with Google sign-in and sponsored tx minimizes user friction in fractional ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FabricFraction.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogFabricFraction /// @notice Enable fractional ownership of rare textile swatches through secure tokenization and seamless onboarding. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogFabricFraction { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FabricFraction` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FabricFraction.sol FabricFraction` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every material micro-ownership action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-fabric-fusion-15 · fashion · Fabric Fusion ===== Pitch: Pin multimedia fabric experiments combining textiles and digital art. Page: https://arcprompts.lovable.app/ideas/fashion-fabric-fusion-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Fabric Fusion" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin multimedia fabric experiments combining textiles and digital art. Discipline: Fashion & Textile Design (mixed media textile records). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS via Pinata supports diverse file formats for mixed media preservation. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FabricFusion.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentFabricFusion /// @notice Pin multimedia fabric experiments combining textiles and digital art. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentFabricFusion { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FabricFusion` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FabricFusion.sol FabricFusion` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a mixed media textile records, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-fabric-nft-atlas-5 · fashion · Fabric NFT Atlas ===== Pitch: Map and mint provenance NFTs for regional and artisanal fabric producers. Page: https://arcprompts.lovable.app/ideas/fashion-fabric-nft-atlas-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Fabric NFT Atlas" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Map and mint provenance NFTs for regional and artisanal fabric producers. Discipline: Fashion & Textile Design (textile sourcing). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens establish verifiable origin proof for fabric supply chains. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FabricNFTAtlas.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title FabricNFTAtlas /// @notice ERC-721 provenance for: Map and mint provenance NFTs for regional and artisanal fabric producers. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FabricNFTAtlas is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("FabricNFTAtlas", "FABRIC") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FabricNFTAtlas` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FabricNFTAtlas.sol FabricNFTAtlas` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a textile sourcing, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-fabric-remix-16 · fashion · Fabric Remix ===== Pitch: Mint NFTs for remixed fabric patterns proving originality and creative lineage. Page: https://arcprompts.lovable.app/ideas/fashion-fabric-remix-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Fabric Remix" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs for remixed fabric patterns proving originality and creative lineage. Discipline: Fashion & Textile Design (textile remixing). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain provenance confirms derivative works anchored to original textile creators. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FabricRemix.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title FabricRemix /// @notice ERC-721 provenance for: Mint NFTs for remixed fabric patterns proving originality and creative lineage. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FabricRemix is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("FabricRemix", "FABRIC") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FabricRemix` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FabricRemix.sol FabricRemix` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a textile remixing, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-fabric-storychain-0 · fashion · Fabric Storychain ===== Pitch: Track textile origins transparently for designers and consumers to verify authenticity. Page: https://arcprompts.lovable.app/ideas/fashion-fabric-storychain-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Fabric Storychain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track textile origins transparently for designers and consumers to verify authenticity. Discipline: Fashion & Textile Design (material provenance). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social enables seamless user authentication and gasless tracing of textile provenance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FabricStorychain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogFabricStorychain /// @notice Track textile origins transparently for designers and consumers to verify authenticity. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogFabricStorychain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FabricStorychain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FabricStorychain.sol FabricStorychain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every material provenance action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-fabric-storyline-0 · fashion · Fabric Storyline ===== Pitch: Securely archive and share original fabric patterns with permanent proof of creativity. Page: https://arcprompts.lovable.app/ideas/fashion-fabric-storyline-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Fabric Storyline" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely archive and share original fabric patterns with permanent proof of creativity. Discipline: Fashion & Textile Design (digital textile archives). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS via Pinata ensures immutable, decentralized storage of design patterns accessible globally. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FabricStoryline.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentFabricStoryline /// @notice Securely archive and share original fabric patterns with permanent proof of creativity. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentFabricStoryline { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FabricStoryline` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FabricStoryline.sol FabricStoryline` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a digital textile archives, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-fabric-swapchain-13 · fashion · Fabric Swapchain ===== Pitch: Decentralize textile swaps between designers via secure tokenized exchanges. Page: https://arcprompts.lovable.app/ideas/fashion-fabric-swapchain-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Fabric Swapchain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Decentralize textile swaps between designers via secure tokenized exchanges. Discipline: Fashion & Textile Design (material bartering). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet and sponsored tx facilitate seamless trades without gas friction. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FabricSwapchain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogFabricSwapchain /// @notice Decentralize textile swaps between designers via secure tokenized exchanges. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogFabricSwapchain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FabricSwapchain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FabricSwapchain.sol FabricSwapchain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every material bartering action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-fabricbatch-chain-14 · fashion · FabricBatch Chain ===== Pitch: Log fabric batch quality certifications on-chain for designer trust and compliance. Page: https://arcprompts.lovable.app/ideas/fashion-fabricbatch-chain-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FabricBatch Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Log fabric batch quality certifications on-chain for designer trust and compliance. Discipline: Fashion & Textile Design (batch quality control). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts enable tamper-proof quality certification transparency. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FabricBatchChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title FabricBatchChain /// @notice Log fabric batch quality certifications on-chain for designer trust and compliance. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FabricBatchChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FabricBatchChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FabricBatchChain.sol FabricBatchChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a batch quality control action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-fabrictrace-ledger-0 · fashion · FabricTrace Ledger ===== Pitch: Track and verify organic fabric sources to assure sustainable fashion claims. Page: https://arcprompts.lovable.app/ideas/fashion-fabrictrace-ledger-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FabricTrace Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track and verify organic fabric sources to assure sustainable fashion claims. Discipline: Fashion & Textile Design (material provenance). Onchain primitive: Arc smart contract. Why this primitive: 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. 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 { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FabricTraceLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FabricTraceLedger.sol FabricTraceLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a material provenance action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-fashion-dao-hub-19 · fashion · Fashion DAO Hub ===== Pitch: Empower designer collectives to govern projects through token voting with easy wallet setup. Page: https://arcprompts.lovable.app/ideas/fashion-fashion-dao-hub-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Fashion DAO Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Empower designer collectives to govern projects through token voting with easy wallet setup. Discipline: Fashion & Textile Design (decentralized fashion governance). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus gasless transactions lower barriers for DAO participation in fashion niches. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FashionDAOHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogFashionDAOHub /// @notice Empower designer collectives to govern projects through token voting with easy wallet setup. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogFashionDAOHub { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FashionDAOHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FashionDAOHub.sol FashionDAOHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every decentralized fashion governance action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-fashion-dna-10 · fashion · Fashion DNA ===== Pitch: Create unique NFTs representing brand values and design DNA to protect brand authenticity. Page: https://arcprompts.lovable.app/ideas/fashion-fashion-dna-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Fashion DNA" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create unique NFTs representing brand values and design DNA to protect brand authenticity. Discipline: Fashion & Textile Design (brand identity). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 minting immutably encodes brand provenance onchain and on IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FashionDNA.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title FashionDNA /// @notice ERC-721 provenance for: Create unique NFTs representing brand values and design DNA to protect brand authenticity. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FashionDNA is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("FashionDNA", "FASHIO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FashionDNA` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FashionDNA.sol FashionDNA` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a brand identity, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-fashion-footprint-20 · fashion · Fashion Footprint ===== Pitch: Pin comprehensive lifecycle data of designs for transparency and accountability. Page: https://arcprompts.lovable.app/ideas/fashion-fashion-footprint-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Fashion Footprint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin comprehensive lifecycle data of designs for transparency and accountability. Discipline: Fashion & Textile Design (design lifecycle tracking). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata ensures immutable archival of design production and usage data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FashionFootprint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentFashionFootprint /// @notice Pin comprehensive lifecycle data of designs for transparency and accountability. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentFashionFootprint { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FashionFootprint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FashionFootprint.sol FashionFootprint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a design lifecycle tracking, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-fashioncollab-dao-11 · fashion · FashionCollab DAO ===== Pitch: Decentralize fashion design collaboration with smart contract governed decision-making. Page: https://arcprompts.lovable.app/ideas/fashion-fashioncollab-dao-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FashionCollab DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Decentralize fashion design collaboration with smart contract governed decision-making. Discipline: Fashion & Textile Design (collaborative design). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enforce ownership and voting rules in collaborative projects. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FashionCollabDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title FashionCollabDAO /// @notice Decentralize fashion design collaboration with smart contract governed decision-making. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FashionCollabDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FashionCollabDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FashionCollabDAO.sol FashionCollabDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a collaborative design action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-fashioncontractor-dao-20 · fashion · FashionContractor DAO ===== Pitch: Manage freelance fashion contracts transparently with on-chain escrow and milestones. Page: https://arcprompts.lovable.app/ideas/fashion-fashioncontractor-dao-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FashionContractor DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Manage freelance fashion contracts transparently with on-chain escrow and milestones. Discipline: Fashion & Textile Design (freelance contract management). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts automate payments and enforce contract terms securely. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FashionContractorDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title FashionContractorDAO /// @notice Manage freelance fashion contracts transparently with on-chain escrow and milestones. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FashionContractorDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FashionContractorDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FashionContractorDAO.sol FashionContractorDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a freelance contract management action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-fit-file-safe-7 · fashion · Fit File Safe ===== Pitch: Store 3D fitting session data securely for consistent tailoring and reuse. Page: https://arcprompts.lovable.app/ideas/fashion-fit-file-safe-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Fit File Safe" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store 3D fitting session data securely for consistent tailoring and reuse. Discipline: Fashion & Textile Design (3D garment fitting). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS pins preserve complex 3D fitting files immutably offchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FitFileSafe.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentFitFileSafe /// @notice Store 3D fitting session data securely for consistent tailoring and reuse. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentFitFileSafe { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FitFileSafe` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FitFileSafe.sol FitFileSafe` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a 3D garment fitting, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-fit-nft-lab-14 · fashion · Fit NFT Lab ===== Pitch: Mint verified NFTs representing customizable fit parameters for digital wardrobe pieces. Page: https://arcprompts.lovable.app/ideas/fashion-fit-nft-lab-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Fit NFT Lab" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint verified NFTs representing customizable fit parameters for digital wardrobe pieces. Discipline: Fashion & Textile Design (virtual fitting). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Immutable tokens anchor each unique fit profile to user and designer provenance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FitNFTLab.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title FitNFTLab /// @notice ERC-721 provenance for: Mint verified NFTs representing customizable fit parameters for digital wardrobe pieces. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FitNFTLab is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("FitNFTLab", "FITNFT") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FitNFTLab` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FitNFTLab.sol FitNFTLab` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a virtual fitting, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-moodboard-ledger-4 · fashion · Moodboard Ledger ===== Pitch: Pin moodboards with images and notes securely to track creative evolution. Page: https://arcprompts.lovable.app/ideas/fashion-moodboard-ledger-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Moodboard Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin moodboards with images and notes securely to track creative evolution. Discipline: Fashion & Textile Design (inspiration curation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata provides permanent storage for evolving, multi-format design inspirations. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MoodboardLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMoodboardLedger /// @notice Pin moodboards with images and notes securely to track creative evolution. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMoodboardLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MoodboardLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MoodboardLedger.sol MoodboardLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a inspiration curation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-moodboard-mint-1 · fashion · Moodboard Mint ===== Pitch: Create and share moodboards that capture design inspirations with onchain credits and permissions. Page: https://arcprompts.lovable.app/ideas/fashion-moodboard-mint-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Moodboard Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create and share moodboards that capture design inspirations with onchain credits and permissions. Discipline: Fashion & Textile Design (collaborative curation). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social allows easy Google sign-in and sponsored tx to enable frictionless moodboard sharing. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MoodboardMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogMoodboardMint /// @notice Create and share moodboards that capture design inspirations with onchain credits and permissions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogMoodboardMint { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MoodboardMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MoodboardMint.sol MoodboardMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every collaborative curation action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-moodboard-mint-13 · fashion · Moodboard Mint ===== Pitch: Securely mint fashion moodboards as NFTs to claim original inspiration and concepts. Page: https://arcprompts.lovable.app/ideas/fashion-moodboard-mint-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Moodboard Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely mint fashion moodboards as NFTs to claim original inspiration and concepts. Discipline: Fashion & Textile Design (concept curation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance mints timestamp and verify artwork or collage originality. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MoodboardMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MoodboardMint /// @notice ERC-721 provenance for: Securely mint fashion moodboards as NFTs to claim original inspiration and concepts. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MoodboardMint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MoodboardMint", "MOODBO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MoodboardMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MoodboardMint.sol MoodboardMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a concept curation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-pattern-nft-forge-21 · fashion · Pattern NFT Forge ===== Pitch: Pin pattern files to IPFS to mint unique, verifiable design NFTs. Page: https://arcprompts.lovable.app/ideas/fashion-pattern-nft-forge-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Pattern NFT Forge" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin pattern files to IPFS to mint unique, verifiable design NFTs. Discipline: Fashion & Textile Design (blockchain pattern minting). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata's IPFS integration enables secure permanent storage for NFT minting. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PatternNFTForge.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPatternNFTForge /// @notice Pin pattern files to IPFS to mint unique, verifiable design NFTs. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPatternNFTForge { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PatternNFTForge` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PatternNFTForge.sol PatternNFTForge` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a blockchain pattern minting, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-pattern-pathway-13 · fashion · Pattern Pathway ===== Pitch: Pin editable pattern files for seamless transfer and secure versioning. Page: https://arcprompts.lovable.app/ideas/fashion-pattern-pathway-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Pattern Pathway" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin editable pattern files for seamless transfer and secure versioning. Discipline: Fashion & Textile Design (seamless pattern sharing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS guarantees immutable and accessible sharing of pattern files. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PatternPathway.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPatternPathway /// @notice Pin editable pattern files for seamless transfer and secure versioning. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPatternPathway { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PatternPathway` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PatternPathway.sol PatternPathway` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a seamless pattern sharing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-pattern-provenance-17 · fashion · Pattern Provenance ===== Pitch: Protect sewing patterns with immutable copyright records accessible via Google login. Page: https://arcprompts.lovable.app/ideas/fashion-pattern-provenance-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Pattern Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Protect sewing patterns with immutable copyright records accessible via Google login. Discipline: Fashion & Textile Design (design copyright). Onchain primitive: Privy social wallet on Arc. Why this primitive: Gasless sponsored tx enable pattern creators to register rights affordably and easily. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PatternProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPatternProvenance /// @notice Protect sewing patterns with immutable copyright records accessible via Google login. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPatternProvenance { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PatternProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PatternProvenance.sol PatternProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every design copyright action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-pattern-provenance-3 · fashion · Pattern Provenance ===== Pitch: Record and verify original textile patterns as immutable digital assets. Page: https://arcprompts.lovable.app/ideas/fashion-pattern-provenance-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Pattern Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record and verify original textile patterns as immutable digital assets. Discipline: Fashion & Textile Design (textile pattern design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT minting binds patterns to creators and timestamps on IPFS metadata. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PatternProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PatternProvenance /// @notice ERC-721 provenance for: Record and verify original textile patterns as immutable digital assets. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PatternProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PatternProvenance", "PATTER") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PatternProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PatternProvenance.sol PatternProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a textile pattern design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-patternproof-chain-18 · fashion · PatternProof Chain ===== Pitch: Verify pattern originality and ownership in decentralized on-chain records. Page: https://arcprompts.lovable.app/ideas/fashion-patternproof-chain-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PatternProof Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Verify pattern originality and ownership in decentralized on-chain records. Discipline: Fashion & Textile Design (pattern authenticity). Onchain primitive: Arc smart contract. Why this primitive: Immutability secures pattern provenance against unauthorized reuse. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PatternProofChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PatternProofChain /// @notice Verify pattern originality and ownership in decentralized on-chain records. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PatternProofChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PatternProofChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PatternProofChain.sol PatternProofChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a pattern authenticity action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-patternshare-3 · fashion · PatternShare ===== Pitch: Securely share and license digital sewing patterns with transparent ownership and usage rights. Page: https://arcprompts.lovable.app/ideas/fashion-patternshare-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PatternShare" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely share and license digital sewing patterns with transparent ownership and usage rights. Discipline: Fashion & Textile Design (digital pattern exchange). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet bootstrapped by PRIVY_APP_ID ensures seamless user access and transaction sponsorship. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PatternShare.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPatternShare /// @notice Securely share and license digital sewing patterns with transparent ownership and usage rights. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPatternShare { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PatternShare` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PatternShare.sol PatternShare` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every digital pattern exchange action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-patternshare-hub-6 · fashion · PatternShare Hub ===== Pitch: License and trade garment patterns securely using blockchain-based smart contracts. Page: https://arcprompts.lovable.app/ideas/fashion-patternshare-hub-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PatternShare Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT License and trade garment patterns securely using blockchain-based smart contracts. Discipline: Fashion & Textile Design (pattern licensing). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts manage pattern ownership, licensing, and royalties efficiently. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PatternShareHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PatternShareHub /// @notice License and trade garment patterns securely using blockchain-based smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PatternShareHub { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PatternShareHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PatternShareHub.sol PatternShareHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a pattern licensing action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-print-proofs-23 · fashion · Print Proofs ===== Pitch: Mint NFTs to prove original fabric print designs’ authorship and exclusivity. Page: https://arcprompts.lovable.app/ideas/fashion-print-proofs-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Print Proofs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs to prove original fabric print designs’ authorship and exclusivity. Discipline: Fashion & Textile Design (fabric print designs). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT minting anchors each print design with verifiable creator metadata. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PrintProofs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PrintProofs /// @notice ERC-721 provenance for: Mint NFTs to prove original fabric print designs’ authorship and exclusivity. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PrintProofs is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PrintProofs", "PRINTP") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PrintProofs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PrintProofs.sol PrintProofs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a fabric print designs, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-print-provenance-11 · fashion · Print Provenance ===== Pitch: Securely pin original print artworks to prevent unauthorized reproduction. Page: https://arcprompts.lovable.app/ideas/fashion-print-provenance-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Print Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely pin original print artworks to prevent unauthorized reproduction. Discipline: Fashion & Textile Design (digital print design). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata provides immutable proofs of print artwork originality on IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PrintProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPrintProvenance /// @notice Securely pin original print artworks to prevent unauthorized reproduction. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPrintProvenance { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PrintProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PrintProvenance.sol PrintProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a digital print design, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-runway-record-15 · fashion · Runway Record ===== Pitch: Mint NFTs documenting runway shows’ looks for authentic digital archiving and resale. Page: https://arcprompts.lovable.app/ideas/fashion-runway-record-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Runway Record" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs documenting runway shows’ looks for authentic digital archiving and resale. Discipline: Fashion & Textile Design (showcase curation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens provide unalterable proof for each unique runway outfit presentation. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/RunwayRecord.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title RunwayRecord /// @notice ERC-721 provenance for: Mint NFTs documenting runway shows’ looks for authentic digital archiving and resale. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract RunwayRecord is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("RunwayRecord", "RUNWAY") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs RunwayRecord` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/RunwayRecord.sol RunwayRecord` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a showcase curation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-runway-replay-1 · fashion · Runway Replay ===== Pitch: Pin high-resolution show images and metadata to create an unalterable runway history. Page: https://arcprompts.lovable.app/ideas/fashion-runway-replay-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Runway Replay" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin high-resolution show images and metadata to create an unalterable runway history. Discipline: Fashion & Textile Design (fashion show documentation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata guarantees permanent, tamper-proof storage for fashion event documentation. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/RunwayReplay.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentRunwayReplay /// @notice Pin high-resolution show images and metadata to create an unalterable runway history. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentRunwayReplay { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs RunwayReplay` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/RunwayReplay.sol RunwayReplay` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a fashion show documentation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-runway-replay-6 · fashion · Runway Replay ===== Pitch: Capture and timestamp runway shows for authenticity and replay rights management. Page: https://arcprompts.lovable.app/ideas/fashion-runway-replay-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Runway Replay" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Capture and timestamp runway shows for authenticity and replay rights management. Discipline: Fashion & Textile Design (event recording). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social login ensures easy user onboarding and timestamping without gas friction. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/RunwayReplay.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogRunwayReplay /// @notice Capture and timestamp runway shows for authenticity and replay rights management. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogRunwayReplay { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs RunwayReplay` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/RunwayReplay.sol RunwayReplay` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every event recording action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-runway-reward-14 · fashion · Runway Reward ===== Pitch: Reward attendance and participation at fashion events with tokenized perks and proof of presence. Page: https://arcprompts.lovable.app/ideas/fashion-runway-reward-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Runway Reward" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Reward attendance and participation at fashion events with tokenized perks and proof of presence. Discipline: Fashion & Textile Design (event incentives). Onchain primitive: Privy social wallet on Arc. Why this primitive: Gasless tx encourages user engagement without friction during event check-ins. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/RunwayReward.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogRunwayReward /// @notice Reward attendance and participation at fashion events with tokenized perks and proof of presence. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogRunwayReward { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs RunwayReward` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/RunwayReward.sol RunwayReward` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every event incentives action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-runwayroyalties-nft-23 · fashion · RunwayRoyalties NFT ===== Pitch: Automate royalty distribution for runway show participants through NFT smart contracts. Page: https://arcprompts.lovable.app/ideas/fashion-runwayroyalties-nft-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "RunwayRoyalties NFT" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Automate royalty distribution for runway show participants through NFT smart contracts. Discipline: Fashion & Textile Design (event royalty management). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts transparently distribute earnings per pre-set terms. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/RunwayRoyaltiesNFT.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title RunwayRoyaltiesNFT /// @notice Automate royalty distribution for runway show participants through NFT smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract RunwayRoyaltiesNFT { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs RunwayRoyaltiesNFT` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/RunwayRoyaltiesNFT.sol RunwayRoyaltiesNFT` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a event royalty management action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-sartorial-social-7 · fashion · Sartorial Social ===== Pitch: Connect fashion creatives with gasless transactions enabling easy contract signing and collaborations. Page: https://arcprompts.lovable.app/ideas/fashion-sartorial-social-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sartorial Social" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Connect fashion creatives with gasless transactions enabling easy contract signing and collaborations. Discipline: Fashion & Textile Design (designer networking). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet with sponsored tx ensures smooth social interaction without gas barrier. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SartorialSocial.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSartorialSocial /// @notice Connect fashion creatives with gasless transactions enabling easy contract signing and collaborations. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSartorialSocial { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SartorialSocial` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SartorialSocial.sol SartorialSocial` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every designer networking action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-silhouette-sync-24 · fashion · Silhouette Sync ===== Pitch: Pin silhouette sketches and metadata for collaborative design iterations. Page: https://arcprompts.lovable.app/ideas/fashion-silhouette-sync-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Silhouette Sync" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin silhouette sketches and metadata for collaborative design iterations. Discipline: Fashion & Textile Design (design silhouette sharing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata allows permanent, accessible storage for evolving design sketches. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SilhouetteSync.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentSilhouetteSync /// @notice Pin silhouette sketches and metadata for collaborative design iterations. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentSilhouetteSync { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SilhouetteSync` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SilhouetteSync.sol SilhouetteSync` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a design silhouette sharing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-stitch-trace-3 · fashion · Stitch Trace ===== Pitch: Record detailed stitch patterns and techniques on-chain to prove craftsmanship. Page: https://arcprompts.lovable.app/ideas/fashion-stitch-trace-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Stitch Trace" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record detailed stitch patterns and techniques on-chain to prove craftsmanship. Discipline: Fashion & Textile Design (garment construction logs). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Decentralized, permanent IPFS pins prevent tampering with design process records. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StitchTrace.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentStitchTrace /// @notice Record detailed stitch patterns and techniques on-chain to prove craftsmanship. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentStitchTrace { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StitchTrace` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StitchTrace.sol StitchTrace` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a garment construction logs, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-style-snapshot-15 · fashion · Style Snapshot ===== Pitch: Securely log daily outfits as NFTs with easy social sign-in and no gas cost. Page: https://arcprompts.lovable.app/ideas/fashion-style-snapshot-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Style Snapshot" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely log daily outfits as NFTs with easy social sign-in and no gas cost. Discipline: Fashion & Textile Design (digital outfit logging). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social and sponsored tx let users mint outfit NFTs without blockchain experience. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StyleSnapshot.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogStyleSnapshot /// @notice Securely log daily outfits as NFTs with easy social sign-in and no gas cost. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogStyleSnapshot { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StyleSnapshot` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StyleSnapshot.sol StyleSnapshot` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every digital outfit logging action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-style-snapshot-18 · fashion · Style Snapshot ===== Pitch: Pin curated influencer style images and metadata for trend analysis. Page: https://arcprompts.lovable.app/ideas/fashion-style-snapshot-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Style Snapshot" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin curated influencer style images and metadata for trend analysis. Discipline: Fashion & Textile Design (fashion influencer galleries). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS via Pinata secures permanent influencer content storage. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StyleSnapshot.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentStyleSnapshot /// @notice Pin curated influencer style images and metadata for trend analysis. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentStyleSnapshot { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StyleSnapshot` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StyleSnapshot.sol StyleSnapshot` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a fashion influencer galleries, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-style-stake-12 · fashion · Style Stake ===== Pitch: Stake tokens to support favorite designers and earn rewards through community votes. Page: https://arcprompts.lovable.app/ideas/fashion-style-stake-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Style Stake" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Stake tokens to support favorite designers and earn rewards through community votes. Discipline: Fashion & Textile Design (community style curation). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet with sponsored tx enables easy staking and reward distribution without user gas burden. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StyleStake.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogStyleStake /// @notice Stake tokens to support favorite designers and earn rewards through community votes. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogStyleStake { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StyleStake` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StyleStake.sol StyleStake` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every community style curation action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-style-storyline-24 · fashion · Style Storyline ===== Pitch: Mint NFTs encoding fashion collection narratives as immutable digital assets. Page: https://arcprompts.lovable.app/ideas/fashion-style-storyline-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Style Storyline" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs encoding fashion collection narratives as immutable digital assets. Discipline: Fashion & Textile Design (fashion storytelling). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 provenance mints protect unique brand stories linked to collections. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StyleStoryline.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title StyleStoryline /// @notice ERC-721 provenance for: Mint NFTs encoding fashion collection narratives as immutable digital assets. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StyleStoryline is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("StyleStoryline", "STYLES") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StyleStoryline` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StyleStoryline.sol StyleStoryline` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a fashion storytelling, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-style-swap-21 · fashion · Style Swap ===== Pitch: Facilitate safe swaps of clothing items with secure tokenized agreements and zero gas fees. Page: https://arcprompts.lovable.app/ideas/fashion-style-swap-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Style Swap" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Facilitate safe swaps of clothing items with secure tokenized agreements and zero gas fees. Discipline: Fashion & Textile Design (peer-to-peer fashion exchange). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet plus sponsored tx encourage user trades by removing blockchain friction. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StyleSwap.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogStyleSwap /// @notice Facilitate safe swaps of clothing items with secure tokenized agreements and zero gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogStyleSwap { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StyleSwap` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StyleSwap.sol StyleSwap` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every peer-to-peer fashion exchange action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-style-vault-2 · fashion · Style Vault ===== Pitch: Create exclusive NFT collections of curated outfits for personalized style portfolios. Page: https://arcprompts.lovable.app/ideas/fashion-style-vault-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Style Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create exclusive NFT collections of curated outfits for personalized style portfolios. Discipline: Fashion & Textile Design (outfit curation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Minting NFTs onchain proves ownership and originality of curated digital styles. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StyleVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title StyleVault /// @notice ERC-721 provenance for: Create exclusive NFT collections of curated outfits for personalized style portfolios. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StyleVault is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("StyleVault", "STYLEV") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StyleVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StyleVault.sol StyleVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a outfit curation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-stylelicense-nft-13 · fashion · StyleLicense NFT ===== Pitch: Mint and trade exclusive style licenses as NFTs to protect designer creativity. Page: https://arcprompts.lovable.app/ideas/fashion-stylelicense-nft-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StyleLicense NFT" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint and trade exclusive style licenses as NFTs to protect designer creativity. Discipline: Fashion & Textile Design (style licensing). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts automate license issuance, transfer, and royalty payments. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StyleLicenseNFT.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title StyleLicenseNFT /// @notice Mint and trade exclusive style licenses as NFTs to protect designer creativity. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StyleLicenseNFT { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StyleLicenseNFT` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StyleLicenseNFT.sol StyleLicenseNFT` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a style licensing action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-styleswap-chain-24 · fashion · StyleSwap Chain ===== Pitch: Trade digital fashion items securely with on-chain ownership and transfer records. Page: https://arcprompts.lovable.app/ideas/fashion-styleswap-chain-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StyleSwap Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Trade digital fashion items securely with on-chain ownership and transfer records. Discipline: Fashion & Textile Design (digital fashion exchange). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts guarantee trustless exchange of digital style assets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StyleSwapChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title StyleSwapChain /// @notice Trade digital fashion items securely with on-chain ownership and transfer records. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StyleSwapChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StyleSwapChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StyleSwapChain.sol StyleSwapChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a digital fashion exchange action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-stylistbooking-chain-8 · fashion · StylistBooking Chain ===== Pitch: Manage stylist-client appointments with transparent, automated escrow and approvals. Page: https://arcprompts.lovable.app/ideas/fashion-stylistbooking-chain-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StylistBooking Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Manage stylist-client appointments with transparent, automated escrow and approvals. Discipline: Fashion & Textile Design (consultation scheduling). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts facilitate secure, trustless booking and payment workflows. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StylistBookingChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title StylistBookingChain /// @notice Manage stylist-client appointments with transparent, automated escrow and approvals. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StylistBookingChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StylistBookingChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StylistBookingChain.sol StylistBookingChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a consultation scheduling action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-supplychain-mint-12 · fashion · SupplyChain Mint ===== Pitch: Record each production stage on-chain for transparent garment supply chains. Page: https://arcprompts.lovable.app/ideas/fashion-supplychain-mint-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SupplyChain Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record each production stage on-chain for transparent garment supply chains. Discipline: Fashion & Textile Design (production tracking). Onchain primitive: Arc smart contract. Why this primitive: Immutable smart contracts provide verified, tamper-proof production logs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SupplyChainMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SupplyChainMint /// @notice Record each production stage on-chain for transparent garment supply chains. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SupplyChainMint { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SupplyChainMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SupplyChainMint.sol SupplyChainMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a production tracking action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-sustainable-stitch-17 · fashion · Sustainable Stitch ===== Pitch: Mint NFTs proving zero-waste garment designs for fashion sustainability verification. Page: https://arcprompts.lovable.app/ideas/fashion-sustainable-stitch-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sustainable Stitch" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs proving zero-waste garment designs for fashion sustainability verification. Discipline: Fashion & Textile Design (zero-waste design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Immutable provenance mints validate zero-waste design authenticity and ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SustainableStitch.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SustainableStitch /// @notice ERC-721 provenance for: Mint NFTs proving zero-waste garment designs for fashion sustainability verification. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SustainableStitch is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SustainableStitch", "SUSTAI") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SustainableStitch` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SustainableStitch.sol SustainableStitch` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a zero-waste design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-sustainlabel-verify-9 · fashion · SustainLabel Verify ===== Pitch: Validate sustainable fashion certifications on-chain for trust and transparency. Page: https://arcprompts.lovable.app/ideas/fashion-sustainlabel-verify-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SustainLabel Verify" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Validate sustainable fashion certifications on-chain for trust and transparency. Discipline: Fashion & Textile Design (eco-label validation). Onchain primitive: Arc smart contract. Why this primitive: Immutable contracts prevent label forgery and enable real-time verification. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SustainLabelVerify.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SustainLabelVerify /// @notice Validate sustainable fashion certifications on-chain for trust and transparency. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SustainLabelVerify { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SustainLabelVerify` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SustainLabelVerify.sol SustainLabelVerify` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a eco-label validation action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-tailor-token-18 · fashion · Tailor Token ===== Pitch: Enable tailors to accept and manage custom orders via tokenized contracts and gasless sign-in. Page: https://arcprompts.lovable.app/ideas/fashion-tailor-token-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Tailor Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable tailors to accept and manage custom orders via tokenized contracts and gasless sign-in. Discipline: Fashion & Textile Design (custom order management). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet with sponsored tx reduces friction for small business adoption. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TailorToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogTailorToken /// @notice Enable tailors to accept and manage custom orders via tokenized contracts and gasless sign-in. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogTailorToken { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TailorToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TailorToken.sol TailorToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every custom order management action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-textile-tokenizer-22 · fashion · Textile Tokenizer ===== Pitch: Convert physical textile assets into digital tokens for trading and provenance tracking. Page: https://arcprompts.lovable.app/ideas/fashion-textile-tokenizer-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Textile Tokenizer" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Convert physical textile assets into digital tokens for trading and provenance tracking. Discipline: Fashion & Textile Design (fabric asset digitization). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet simplifies token creation and sponsored tx removes cost barriers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TextileTokenizer.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogTextileTokenizer /// @notice Convert physical textile assets into digital tokens for trading and provenance tracking. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogTextileTokenizer { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TextileTokenizer` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TextileTokenizer.sol TextileTokenizer` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every fabric asset digitization action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-textilewaste-nft-17 · fashion · TextileWaste NFT ===== Pitch: Issue NFTs certifying textile waste recycling to promote circular fashion. Page: https://arcprompts.lovable.app/ideas/fashion-textilewaste-nft-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "TextileWaste NFT" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Issue NFTs certifying textile waste recycling to promote circular fashion. Discipline: Fashion & Textile Design (waste management). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts track and authenticate waste recycling steps publicly. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TextileWasteNFT.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title TextileWasteNFT /// @notice Issue NFTs certifying textile waste recycling to promote circular fashion. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TextileWasteNFT { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TextileWasteNFT` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TextileWasteNFT.sol TextileWasteNFT` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a waste management action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-texture-token-6 · fashion · Texture Token ===== Pitch: Pin high-res texture samples for sharing and verifying textile uniqueness. Page: https://arcprompts.lovable.app/ideas/fashion-texture-token-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Texture Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin high-res texture samples for sharing and verifying textile uniqueness. Discipline: Fashion & Textile Design (fabric texture archives). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata ensures permanent, decentralized storage of texture image data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TextureToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentTextureToken /// @notice Pin high-res texture samples for sharing and verifying textile uniqueness. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentTextureToken { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TextureToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TextureToken.sol TextureToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a fabric texture archives, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-thread-legacy-0 · fashion · Thread Legacy ===== Pitch: Authenticate fabric origins transparently for sustainable fashion designers. Page: https://arcprompts.lovable.app/ideas/fashion-thread-legacy-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Thread Legacy" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate fabric origins transparently for sustainable fashion designers. Discipline: Fashion & Textile Design (fabric provenance). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance mint ensures immutable proof of fabric source and authenticity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ThreadLegacy.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ThreadLegacy /// @notice ERC-721 provenance for: Authenticate fabric origins transparently for sustainable fashion designers. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ThreadLegacy is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ThreadLegacy", "THREAD") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ThreadLegacy` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ThreadLegacy.sol ThreadLegacy` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a fabric provenance, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-thread-token-19 · fashion · Thread Token ===== Pitch: Pin yarn samples and thread color specs for easy supplier access. Page: https://arcprompts.lovable.app/ideas/fashion-thread-token-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Thread Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin yarn samples and thread color specs for easy supplier access. Discipline: Fashion & Textile Design (yarn and thread digital catalogs). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s permanent storage protects detailed yarn metadata and images. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ThreadToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentThreadToken /// @notice Pin yarn samples and thread color specs for easy supplier access. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentThreadToken { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ThreadToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ThreadToken.sol ThreadToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a yarn and thread digital catalogs, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-trend-lineage-5 · fashion · Trend Lineage ===== Pitch: Archive trend data and visuals to verify origin and evolution of styles. Page: https://arcprompts.lovable.app/ideas/fashion-trend-lineage-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Trend Lineage" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Archive trend data and visuals to verify origin and evolution of styles. Discipline: Fashion & Textile Design (fashion trend mapping). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS pins maintain unchangeable records of trend sources and timelines. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TrendLineage.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentTrendLineage /// @notice Archive trend data and visuals to verify origin and evolution of styles. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentTrendLineage { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TrendLineage` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TrendLineage.sol TrendLineage` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a fashion trend mapping, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-trend-tokenizer-9 · fashion · Trend Tokenizer ===== Pitch: Mint data-driven trend insights as NFTs to securely sell forecasting reports. Page: https://arcprompts.lovable.app/ideas/fashion-trend-tokenizer-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Trend Tokenizer" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint data-driven trend insights as NFTs to securely sell forecasting reports. Discipline: Fashion & Textile Design (fashion forecasting). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance ensures originality and ownership of proprietary trend data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TrendTokenizer.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title TrendTokenizer /// @notice ERC-721 provenance for: Mint data-driven trend insights as NFTs to securely sell forecasting reports. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TrendTokenizer is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("TrendTokenizer", "TRENDT") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TrendTokenizer` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TrendTokenizer.sol TrendTokenizer` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a fashion forecasting, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-trendsignal-oracles-5 · fashion · TrendSignal Oracles ===== Pitch: Aggregate verified sales and social data on-chain to predict emerging fashion trends. Page: https://arcprompts.lovable.app/ideas/fashion-trendsignal-oracles-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "TrendSignal Oracles" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Aggregate verified sales and social data on-chain to predict emerging fashion trends. Discipline: Fashion & Textile Design (trend prediction). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts automate transparent, tamper-proof data aggregation via oracles. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TrendSignalOracles.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title TrendSignalOracles /// @notice Aggregate verified sales and social data on-chain to predict emerging fashion trends. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TrendSignalOracles { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TrendSignalOracles` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TrendSignalOracles.sol TrendSignalOracles` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a trend prediction action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-trendtoken-vault-2 · fashion · TrendToken Vault ===== Pitch: Collect and verify emerging fashion trends via community consensus and rewarded participation. Page: https://arcprompts.lovable.app/ideas/fashion-trendtoken-vault-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "TrendToken Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Collect and verify emerging fashion trends via community consensus and rewarded participation. Discipline: Fashion & Textile Design (trend validation). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored tx enable users to vote on trends without gas fees, increasing engagement. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TrendTokenVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogTrendTokenVault /// @notice Collect and verify emerging fashion trends via community consensus and rewarded participation. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogTrendTokenVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TrendTokenVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TrendTokenVault.sol TrendTokenVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every trend validation action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-upcycle-proof-11 · fashion · Upcycle Proof ===== Pitch: Mint NFTs for upcycled garment transformations as proof of sustainable creative ownership. Page: https://arcprompts.lovable.app/ideas/fashion-upcycle-proof-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Upcycle Proof" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs for upcycled garment transformations as proof of sustainable creative ownership. Discipline: Fashion & Textile Design (sustainable design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain tokens give verifiable provenance for altered or recycled fashion pieces. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/UpcycleProof.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title UpcycleProof /// @notice ERC-721 provenance for: Mint NFTs for upcycled garment transformations as proof of sustainable creative ownership. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract UpcycleProof is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("UpcycleProof", "UPCYCL") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs UpcycleProof` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/UpcycleProof.sol UpcycleProof` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a sustainable design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-upcycleproof-chain-4 · fashion · UpcycleProof Chain ===== Pitch: Authenticate recycled textiles’ origin to validate upcycled fashion products. Page: https://arcprompts.lovable.app/ideas/fashion-upcycleproof-chain-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "UpcycleProof Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate recycled textiles’ origin to validate upcycled fashion products. Discipline: Fashion & Textile Design (recycled textile verification). Onchain primitive: Arc smart contract. Why this primitive: Onchain records provide immutable proof of recycling steps and sources. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/UpcycleProofChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title UpcycleProofChain /// @notice Authenticate recycled textiles’ origin to validate upcycled fashion products. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract UpcycleProofChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs UpcycleProofChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/UpcycleProofChain.sol UpcycleProofChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a recycled textile verification action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-vintage-vault-20 · fashion · Vintage Vault ===== Pitch: Authenticate vintage fashion pieces via onchain certificates linked to verified user profiles. Page: https://arcprompts.lovable.app/ideas/fashion-vintage-vault-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Vintage Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate vintage fashion pieces via onchain certificates linked to verified user profiles. Discipline: Fashion & Textile Design (authenticity verification). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy Google sign-in and sponsored tx enable user-friendly provenance verification. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VintageVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogVintageVault /// @notice Authenticate vintage fashion pieces via onchain certificates linked to verified user profiles. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogVintageVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VintageVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VintageVault.sol VintageVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every authenticity verification action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-virtual-trychain-9 · fashion · Virtual Trychain ===== Pitch: Authenticate virtual try-on sessions and purchases transparently for customers and brands. Page: https://arcprompts.lovable.app/ideas/fashion-virtual-trychain-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Virtual Trychain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate virtual try-on sessions and purchases transparently for customers and brands. Discipline: Fashion & Textile Design (try-on verification). Onchain primitive: Privy social wallet on Arc. Why this primitive: Onchain recording with gasless transactions prevents user drop-off during authentication. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VirtualTrychain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogVirtualTrychain /// @notice Authenticate virtual try-on sessions and purchases transparently for customers and brands. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogVirtualTrychain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VirtualTrychain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VirtualTrychain.sol VirtualTrychain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every try-on verification action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-virtual-vogue-vault-22 · fashion · Virtual Vogue Vault ===== Pitch: Mint NFT archives of vintage and digital fashion pieces for authenticated preservation. Page: https://arcprompts.lovable.app/ideas/fashion-virtual-vogue-vault-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Virtual Vogue Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFT archives of vintage and digital fashion pieces for authenticated preservation. Discipline: Fashion & Textile Design (digital fashion archives). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens link archival IPFS content immutably to creators and curators. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VirtualVogueVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title VirtualVogueVault /// @notice ERC-721 provenance for: Mint NFT archives of vintage and digital fashion pieces for authenticated preservation. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VirtualVogueVault is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("VirtualVogueVault", "VIRTUA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VirtualVogueVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VirtualVogueVault.sol VirtualVogueVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a digital fashion archives, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-virtualfitting-nft-3 · fashion · VirtualFitting NFT ===== Pitch: Create unique virtual fitting sessions minted as NFTs for secure client access. Page: https://arcprompts.lovable.app/ideas/fashion-virtualfitting-nft-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VirtualFitting NFT" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create unique virtual fitting sessions minted as NFTs for secure client access. Discipline: Fashion & Textile Design (virtual try-on). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts manage exclusive session access and ownership transparently. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VirtualFittingNFT.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title VirtualFittingNFT /// @notice Create unique virtual fitting sessions minted as NFTs for secure client access. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VirtualFittingNFT { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VirtualFittingNFT` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VirtualFittingNFT.sol VirtualFittingNFT` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a virtual try-on action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-virtualrunway-dao-16 · fashion · VirtualRunway DAO ===== Pitch: Host decentralized, verifiable virtual fashion events with transparent gatekeeping. Page: https://arcprompts.lovable.app/ideas/fashion-virtualrunway-dao-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VirtualRunway DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Host decentralized, verifiable virtual fashion events with transparent gatekeeping. Discipline: Fashion & Textile Design (digital fashion shows). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts control access, voting, and payment distribution securely. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VirtualRunwayDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title VirtualRunwayDAO /// @notice Host decentralized, verifiable virtual fashion events with transparent gatekeeping. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VirtualRunwayDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VirtualRunwayDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VirtualRunwayDAO.sol VirtualRunwayDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a digital fashion shows action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-wearable-royalties-5 · fashion · Wearable Royalties ===== Pitch: Automatically distribute royalties for digital fashion assets across creators and brands. Page: https://arcprompts.lovable.app/ideas/fashion-wearable-royalties-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Wearable Royalties" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Automatically distribute royalties for digital fashion assets across creators and brands. Discipline: Fashion & Textile Design (digital fashion royalties). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored transactions facilitate gas-free royalty payments triggered by user activity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/WearableRoyalties.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogWearableRoyalties /// @notice Automatically distribute royalties for digital fashion assets across creators and brands. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogWearableRoyalties { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs WearableRoyalties` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/WearableRoyalties.sol WearableRoyalties` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every digital fashion royalties action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-wearable-wallet-16 · fashion · Wearable Wallet ===== Pitch: Pin detailed digital wardrobe inventories with immutable metadata. Page: https://arcprompts.lovable.app/ideas/fashion-wearable-wallet-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Wearable Wallet" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin detailed digital wardrobe inventories with immutable metadata. Discipline: Fashion & Textile Design (digital wardrobe management). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata ensures permanent availability and proof of digital clothing assets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/WearableWallet.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentWearableWallet /// @notice Pin detailed digital wardrobe inventories with immutable metadata. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentWearableWallet { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs WearableWallet` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/WearableWallet.sol WearableWallet` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a digital wardrobe management, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-wearablestats-token-15 · fashion · WearableStats Token ===== Pitch: Tokenize data from smart textiles to reward sustainable usage behaviors. Page: https://arcprompts.lovable.app/ideas/fashion-wearablestats-token-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "WearableStats Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Tokenize data from smart textiles to reward sustainable usage behaviors. Discipline: Fashion & Textile Design (performance textiles). Onchain primitive: Arc smart contract. Why this primitive: Onchain tokens enable transparent reward systems linked to textile usage data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/WearableStatsToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title WearableStatsToken /// @notice Tokenize data from smart textiles to reward sustainable usage behaviors. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract WearableStatsToken { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs WearableStatsToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/WearableStatsToken.sol WearableStatsToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a performance textiles action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA fashion-weave-witness-8 · fashion · Weave Witness ===== Pitch: Mint NFTs to certify handcrafted fabric authenticity and artisan identity. Page: https://arcprompts.lovable.app/ideas/fashion-weave-witness-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Weave Witness" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs to certify handcrafted fabric authenticity and artisan identity. Discipline: Fashion & Textile Design (handloom textiles). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs serve as tamper-proof certificates linking artisans to their handwoven products. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/WeaveWitness.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title WeaveWitness /// @notice ERC-721 provenance for: Mint NFTs to certify handcrafted fabric authenticity and artisan identity. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract WeaveWitness is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("WeaveWitness", "WEAVEW") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs WeaveWitness` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/WeaveWitness.sol WeaveWitness` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a handloom textiles, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-anim-badge-9 · film-animation · Anim Badge ===== Pitch: Award and verify animation skills and course completions with gasless onchain badges and wallets. Page: https://arcprompts.lovable.app/ideas/film-animation-anim-badge-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Anim Badge" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Award and verify animation skills and course completions with gasless onchain badges and wallets. Discipline: Filmmaking & Animation (skill certification). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets and sponsored tx remove friction from issuing and verifying credentials socially. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimBadge.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogAnimBadge /// @notice Award and verify animation skills and course completions with gasless onchain badges and wallets. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogAnimBadge { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimBadge` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimBadge.sol AnimBadge` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every skill certification action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-anim-guild-22 · film-animation · Anim Guild ===== Pitch: Create animator guilds with onchain membership and governance via gasless Privy wallet interactions. Page: https://arcprompts.lovable.app/ideas/film-animation-anim-guild-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Anim Guild" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create animator guilds with onchain membership and governance via gasless Privy wallet interactions. Discipline: Filmmaking & Animation (community governance). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy’s social wallet and sponsored tx enable barrier-free participation without gas for members. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimGuild.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogAnimGuild /// @notice Create animator guilds with onchain membership and governance via gasless Privy wallet interactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogAnimGuild { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimGuild` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimGuild.sol AnimGuild` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every community governance action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-anim-rights-vault-5 · film-animation · Anim Rights Vault ===== Pitch: Securely store and prove animation IP ownership and usage rights with Privy-bootstrapped wallets. Page: https://arcprompts.lovable.app/ideas/film-animation-anim-rights-vault-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Anim Rights Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely store and prove animation IP ownership and usage rights with Privy-bootstrapped wallets. Discipline: Filmmaking & Animation (animation IP protection). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet’s ease and gasless tx ensure artists can protect IP without blockchain friction. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimRightsVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogAnimRightsVault /// @notice Securely store and prove animation IP ownership and usage rights with Privy-bootstrapped wallets. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogAnimRightsVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimRightsVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimRightsVault.sol AnimRightsVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every animation IP protection action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animateswap-11 · film-animation · AnimateSwap ===== Pitch: Trade animation asset snippets instantly with gasless wallet transactions and social sharing built-in. Page: https://arcprompts.lovable.app/ideas/film-animation-animateswap-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimateSwap" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Trade animation asset snippets instantly with gasless wallet transactions and social sharing built-in. Discipline: Filmmaking & Animation (asset trading). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social wallets with sponsored transactions allow users to swap assets without gas payment barriers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimateSwap.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogAnimateSwap /// @notice Trade animation asset snippets instantly with gasless wallet transactions and social sharing built-in. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogAnimateSwap { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimateSwap` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimateSwap.sol AnimateSwap` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every asset trading action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animatic-drops-1 · film-animation · Animatic Drops ===== Pitch: Distribute animatics securely with embedded wallets for easy user access and sponsored preview transactions. Page: https://arcprompts.lovable.app/ideas/film-animation-animatic-drops-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Animatic Drops" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Distribute animatics securely with embedded wallets for easy user access and sponsored preview transactions. Discipline: Filmmaking & Animation (animatic distribution). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet and sponsored tx provide frictionless previews and social sharing without gas fees. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimaticDrops.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogAnimaticDrops /// @notice Distribute animatics securely with embedded wallets for easy user access and sponsored preview transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogAnimaticDrops { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimaticDrops` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimaticDrops.sol AnimaticDrops` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every animatic distribution action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animbadge-awards-19 · film-animation · AnimBadge Awards ===== Pitch: Issue blockchain-verified badges and awards recognizing animator skills and achievements. Page: https://arcprompts.lovable.app/ideas/film-animation-animbadge-awards-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimBadge Awards" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Issue blockchain-verified badges and awards recognizing animator skills and achievements. Discipline: Filmmaking & Animation (industry recognition). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts create immutable, forgery-proof industry credentials. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimBadgeAwards.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AnimBadgeAwards /// @notice Issue blockchain-verified badges and awards recognizing animator skills and achievements. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimBadgeAwards { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimBadgeAwards` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimBadgeAwards.sol AnimBadgeAwards` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a industry recognition action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animchain-feedback-17 · film-animation · AnimChain Feedback ===== Pitch: Capture and timestamp animator feedback on projects securely and gaslessly with Privy-based wallets. Page: https://arcprompts.lovable.app/ideas/film-animation-animchain-feedback-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimChain Feedback" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Capture and timestamp animator feedback on projects securely and gaslessly with Privy-based wallets. Discipline: Filmmaking & Animation (creative feedback loops). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet and sponsored tx simplify onchain feedback without user gas awareness. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimChainFeedback.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogAnimChainFeedback /// @notice Capture and timestamp animator feedback on projects securely and gaslessly with Privy-based wallets. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogAnimChainFeedback { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimChainFeedback` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimChainFeedback.sol AnimChainFeedback` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every creative feedback loops action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animcollab-chain-24 · film-animation · AnimCollab Chain ===== Pitch: Coordinate multi-artist animation projects with onchain task assignments and progress tracking. Page: https://arcprompts.lovable.app/ideas/film-animation-animcollab-chain-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimCollab Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Coordinate multi-artist animation projects with onchain task assignments and progress tracking. Discipline: Filmmaking & Animation (collaborative workflows). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enable secure, transparent collaboration records and payments. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimCollabChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AnimCollabChain /// @notice Coordinate multi-artist animation projects with onchain task assignments and progress tracking. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimCollabChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimCollabChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimCollabChain.sol AnimCollabChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a collaborative workflows action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animcollab-hub-15 · film-animation · AnimCollab Hub ===== Pitch: Synchronize animation team files by pinning project manifests on IPFS for secure multiuser updates. Page: https://arcprompts.lovable.app/ideas/film-animation-animcollab-hub-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimCollab Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Synchronize animation team files by pinning project manifests on IPFS for secure multiuser updates. Discipline: Filmmaking & Animation (team project syncing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT provides immutable, shared project state storage decentralized over IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimCollabHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAnimCollabHub /// @notice Synchronize animation team files by pinning project manifests on IPFS for secure multiuser updates. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAnimCollabHub { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimCollabHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimCollabHub.sol AnimCollabHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a team project syncing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animcontest-dao-15 · film-animation · AnimContest DAO ===== Pitch: Run transparent animation contests with onchain submission tracking and automated prize distribution. Page: https://arcprompts.lovable.app/ideas/film-animation-animcontest-dao-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimContest DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Run transparent animation contests with onchain submission tracking and automated prize distribution. Discipline: Filmmaking & Animation (competition governance). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts guarantee fair, verifiable competition results and payouts. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimContestDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AnimContestDAO /// @notice Run transparent animation contests with onchain submission tracking and automated prize distribution. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimContestDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimContestDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimContestDAO.sol AnimContestDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a competition governance action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animcred-score-6 · film-animation · AnimCred Score ===== Pitch: Build transparent, verifiable reputation scores for filmmakers and animators using onchain activity. Page: https://arcprompts.lovable.app/ideas/film-animation-animcred-score-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimCred Score" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Build transparent, verifiable reputation scores for filmmakers and animators using onchain activity. Discipline: Filmmaking & Animation (creator reputation). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts securely aggregate and verify user contributions and feedback. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimCredScore.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AnimCredScore /// @notice Build transparent, verifiable reputation scores for filmmakers and animators using onchain activity. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimCredScore { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimCredScore` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimCredScore.sol AnimCredScore` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a creator reputation action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animcrew-chain-14 · film-animation · AnimCrew Chain ===== Pitch: Manage and reward animation production teams with gasless social wallet-based task and payout tracking. Page: https://arcprompts.lovable.app/ideas/film-animation-animcrew-chain-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimCrew Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Manage and reward animation production teams with gasless social wallet-based task and payout tracking. Discipline: Filmmaking & Animation (team collaboration). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet plus sponsored transactions enables smooth team coordination without gas costs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimCrewChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogAnimCrewChain /// @notice Manage and reward animation production teams with gasless social wallet-based task and payout tracking. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogAnimCrewChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimCrewChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimCrewChain.sol AnimCrewChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every team collaboration action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animcycle-provenance-24 · film-animation · AnimCycle Provenance ===== Pitch: Mint NFT walk and run cycles to protect proprietary animation loops and licensing. Page: https://arcprompts.lovable.app/ideas/film-animation-animcycle-provenance-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimCycle Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFT walk and run cycles to protect proprietary animation loops and licensing. Discipline: Filmmaking & Animation (loop cycles). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens secure unique cycle animations with creator provenance on Sepolia. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimCycleProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title AnimCycleProvenance /// @notice ERC-721 provenance for: Mint NFT walk and run cycles to protect proprietary animation loops and licensing. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimCycleProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("AnimCycleProvenance", "ANIMCY") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimCycleProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimCycleProvenance.sol AnimCycleProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a loop cycles, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animdata-store-10 · film-animation · AnimData Store ===== Pitch: Pin keyframe data and animation parameters to IPFS for permanent archival and collaboration. Page: https://arcprompts.lovable.app/ideas/film-animation-animdata-store-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimData Store" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin keyframe data and animation parameters to IPFS for permanent archival and collaboration. Discipline: Filmmaking & Animation (animation data archival). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT secures permanent JSON pinning of crucial animation datasets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimDataStore.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAnimDataStore /// @notice Pin keyframe data and animation parameters to IPFS for permanent archival and collaboration. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAnimDataStore { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimDataStore` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimDataStore.sol AnimDataStore` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a animation data archival, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animfeedback-nft-20 · film-animation · AnimFeedback NFT ===== Pitch: Monetize animation feedback as NFTs with Privy wallets enabling gas-free minting and transactions. Page: https://arcprompts.lovable.app/ideas/film-animation-animfeedback-nft-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimFeedback NFT" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Monetize animation feedback as NFTs with Privy wallets enabling gas-free minting and transactions. Discipline: Filmmaking & Animation (feedback monetization). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy’s embedded wallet and sponsored tx ensure easy, gasless NFT creation for feedback tokens. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimFeedbackNFT.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogAnimFeedbackNFT /// @notice Monetize animation feedback as NFTs with Privy wallets enabling gas-free minting and transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogAnimFeedbackNFT { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimFeedbackNFT` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimFeedbackNFT.sol AnimFeedbackNFT` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every feedback monetization action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animfont-locker-18 · film-animation · AnimFont Locker ===== Pitch: Pin custom fonts and typographic assets to IPFS for permanent reuse in animated titles. Page: https://arcprompts.lovable.app/ideas/film-animation-animfont-locker-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimFont Locker" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin custom fonts and typographic assets to IPFS for permanent reuse in animated titles. Discipline: Filmmaking & Animation (typography asset vault). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT ensures immutable hosting of font files and JSON usage data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimFontLocker.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAnimFontLocker /// @notice Pin custom fonts and typographic assets to IPFS for permanent reuse in animated titles. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAnimFontLocker { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimFontLocker` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimFontLocker.sol AnimFontLocker` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a typography asset vault, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animframe-swap-23 · film-animation · AnimFrame Swap ===== Pitch: Pin and trade individual animation frames on IPFS for verified ownership and provenance. Page: https://arcprompts.lovable.app/ideas/film-animation-animframe-swap-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimFrame Swap" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin and trade individual animation frames on IPFS for verified ownership and provenance. Discipline: Filmmaking & Animation (frame trading marketplace). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT secures immutable, decentralized frame asset pinning with permanent CIDs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimFrameSwap.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAnimFrameSwap /// @notice Pin and trade individual animation frames on IPFS for verified ownership and provenance. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAnimFrameSwap { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimFrameSwap` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimFrameSwap.sol AnimFrameSwap` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a frame trading marketplace, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animframe-tag-14 · film-animation · AnimFrame Tag ===== Pitch: Mint keyframes as NFTs to prove original frame animation ownership and usage. Page: https://arcprompts.lovable.app/ideas/film-animation-animframe-tag-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimFrame Tag" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint keyframes as NFTs to prove original frame animation ownership and usage. Discipline: Filmmaking & Animation (frame-by-frame animation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens ensure unique keyframe provenance with IPFS CID linkage. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimFrameTag.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title AnimFrameTag /// @notice ERC-721 provenance for: Mint keyframes as NFTs to prove original frame animation ownership and usage. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimFrameTag is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("AnimFrameTag", "ANIMFR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimFrameTag` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimFrameTag.sol AnimFrameTag` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a frame-by-frame animation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animframe-trade-20 · film-animation · AnimFrame Trade ===== Pitch: License individual animation frames securely and transparently using blockchain smart contracts. Page: https://arcprompts.lovable.app/ideas/film-animation-animframe-trade-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimFrame Trade" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT License individual animation frames securely and transparently using blockchain smart contracts. Discipline: Filmmaking & Animation (frame licensing). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts automate license terms and prove usage rights immutably. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimFrameTrade.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AnimFrameTrade /// @notice License individual animation frames securely and transparently using blockchain smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimFrameTrade { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimFrameTrade` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimFrameTrade.sol AnimFrameTrade` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a frame licensing action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animguild-dao-8 · film-animation · AnimGuild DAO ===== Pitch: Empower animation communities with decentralized autonomous organizations for project decisions. Page: https://arcprompts.lovable.app/ideas/film-animation-animguild-dao-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimGuild DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Empower animation communities with decentralized autonomous organizations for project decisions. Discipline: Filmmaking & Animation (community governance). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts enable transparent, rule-based voting and fund allocation. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimGuildDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AnimGuildDAO /// @notice Empower animation communities with decentralized autonomous organizations for project decisions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimGuildDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimGuildDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimGuildDAO.sol AnimGuildDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a community governance action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animlesson-chain-14 · film-animation · AnimLesson Chain ===== Pitch: Certify animation course completions and skills with blockchain-verified digital diplomas. Page: https://arcprompts.lovable.app/ideas/film-animation-animlesson-chain-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimLesson Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Certify animation course completions and skills with blockchain-verified digital diplomas. Discipline: Filmmaking & Animation (educational credentialing). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide tamper-proof certificates accessible globally and instantly. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimLessonChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AnimLessonChain /// @notice Certify animation course completions and skills with blockchain-verified digital diplomas. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimLessonChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimLessonChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimLessonChain.sol AnimLessonChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a educational credentialing action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animloop-provenance-10 · film-animation · AnimLoop Provenance ===== Pitch: Create provenance-backed NFT loops ensuring traceable animation reuse and credit. Page: https://arcprompts.lovable.app/ideas/film-animation-animloop-provenance-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimLoop Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create provenance-backed NFT loops ensuring traceable animation reuse and credit. Discipline: Filmmaking & Animation (animation loops). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: On-chain NFT minting certifies unique loop animations with creator data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimLoopProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title AnimLoopProvenance /// @notice ERC-721 provenance for: Create provenance-backed NFT loops ensuring traceable animation reuse and credit. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimLoopProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("AnimLoopProvenance", "ANIMLO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimLoopProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimLoopProvenance.sol AnimLoopProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a animation loops, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animmetamanager-23 · film-animation · AnimMetaManager ===== Pitch: Securely store and update animation metadata onchain to maintain provenance and rights data. Page: https://arcprompts.lovable.app/ideas/film-animation-animmetamanager-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimMetaManager" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely store and update animation metadata onchain to maintain provenance and rights data. Discipline: Filmmaking & Animation (metadata control). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts ensure metadata integrity and public verification. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimMetaManager.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AnimMetaManager /// @notice Securely store and update animation metadata onchain to maintain provenance and rights data. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimMetaManager { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimMetaManager` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimMetaManager.sol AnimMetaManager` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a metadata control action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animmint-vault-1 · film-animation · AnimMint Vault ===== Pitch: Create NFT-verified character animations to authenticate originality and usage rights. Page: https://arcprompts.lovable.app/ideas/film-animation-animmint-vault-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimMint Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create NFT-verified character animations to authenticate originality and usage rights. Discipline: Filmmaking & Animation (character animation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 minting links unique animation CIDs with creator identity 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimMintVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title AnimMintVault /// @notice ERC-721 provenance for: Create NFT-verified character animations to authenticate originality and usage rights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimMintVault is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("AnimMintVault", "ANIMMI") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimMintVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimMintVault.sol AnimMintVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a character animation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animpitch-fund-21 · film-animation · AnimPitch Fund ===== Pitch: Pitch animation projects to decentralized investors with onchain proposals and funding escrow. Page: https://arcprompts.lovable.app/ideas/film-animation-animpitch-fund-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimPitch Fund" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pitch animation projects to decentralized investors with onchain proposals and funding escrow. Discipline: Filmmaking & Animation (project pitching). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts ensure transparent funding and project milestone tracking. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimPitchFund.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AnimPitchFund /// @notice Pitch animation projects to decentralized investors with onchain proposals and funding escrow. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimPitchFund { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimPitchFund` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimPitchFund.sol AnimPitchFund` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a project pitching action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animpitch-token-23 · film-animation · AnimPitch Token ===== Pitch: Mint NFT pitch decks for animation projects guaranteeing originality and creator rights. Page: https://arcprompts.lovable.app/ideas/film-animation-animpitch-token-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimPitch Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFT pitch decks for animation projects guaranteeing originality and creator rights. Discipline: Filmmaking & Animation (project pitching). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 minting provides immutable pitch asset authentication 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimPitchToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title AnimPitchToken /// @notice ERC-721 provenance for: Mint NFT pitch decks for animation projects guaranteeing originality and creator rights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimPitchToken is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("AnimPitchToken", "ANIMPI") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimPitchToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimPitchToken.sol AnimPitchToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a project pitching, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animproof-timestamp-10 · film-animation · AnimProof Timestamp ===== Pitch: Timestamp animation works to prove creation dates and prevent IP disputes with onchain records. Page: https://arcprompts.lovable.app/ideas/film-animation-animproof-timestamp-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimProof Timestamp" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Timestamp animation works to prove creation dates and prevent IP disputes with onchain records. Discipline: Filmmaking & Animation (work timestamping). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts create immutable, verifiable proof of creation time. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimProofTimestamp.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AnimProofTimestamp /// @notice Timestamp animation works to prove creation dates and prevent IP disputes with onchain records. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimProofTimestamp { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimProofTimestamp` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimProofTimestamp.sol AnimProofTimestamp` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a work timestamping action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animprops-ledger-8 · film-animation · AnimProps Ledger ===== Pitch: Immutable pinning of prop designs on IPFS enables verified ownership and reuse across projects. Page: https://arcprompts.lovable.app/ideas/film-animation-animprops-ledger-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimProps Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Immutable pinning of prop designs on IPFS enables verified ownership and reuse across projects. Discipline: Filmmaking & Animation (prop asset management). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT allows permanent pinning of image and JSON manifests representing props. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimPropsLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAnimPropsLedger /// @notice Immutable pinning of prop designs on IPFS enables verified ownership and reuse across projects. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAnimPropsLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimPropsLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimPropsLedger.sol AnimPropsLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a prop asset management, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animroyalties-17 · film-animation · AnimRoyalties ===== Pitch: Automate royalty payments for animation collaborators with transparent, onchain smart contracts. Page: https://arcprompts.lovable.app/ideas/film-animation-animroyalties-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimRoyalties" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Automate royalty payments for animation collaborators with transparent, onchain smart contracts. Discipline: Filmmaking & Animation (royalty distribution). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enforce preset royalty splits and instant payouts securely. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimRoyalties.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AnimRoyalties /// @notice Automate royalty payments for animation collaborators with transparent, onchain smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimRoyalties { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimRoyalties` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimRoyalties.sol AnimRoyalties` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a royalty distribution action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animscene-sync-2 · film-animation · AnimScene Sync ===== Pitch: Automatically pin scene JSON manifests to IPFS to track animation iterations and changes. Page: https://arcprompts.lovable.app/ideas/film-animation-animscene-sync-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimScene Sync" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Automatically pin scene JSON manifests to IPFS to track animation iterations and changes. Discipline: Filmmaking & Animation (scene version control). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT offers secure, immutable scene versioning independent of centralized servers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimSceneSync.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAnimSceneSync /// @notice Automatically pin scene JSON manifests to IPFS to track animation iterations and changes. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAnimSceneSync { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimSceneSync` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimSceneSync.sol AnimSceneSync` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a scene version control, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animscript-oracles-16 · film-animation · AnimScript Oracles ===== Pitch: Use blockchain oracles to verify external animation script data integrity and authenticity onchain. Page: https://arcprompts.lovable.app/ideas/film-animation-animscript-oracles-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimScript Oracles" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Use blockchain oracles to verify external animation script data integrity and authenticity onchain. Discipline: Filmmaking & Animation (script verification). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts integrate external data securely with verifiable authenticity via oracles. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimScriptOracles.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AnimScriptOracles /// @notice Use blockchain oracles to verify external animation script data integrity and authenticity onchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimScriptOracles { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimScriptOracles` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimScriptOracles.sol AnimScriptOracles` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a script verification action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animscript-pin-14 · film-animation · AnimScript Pin ===== Pitch: Pin scripts and screenplays as JSON manifests on IPFS for verifiable, collaborative storytelling. Page: https://arcprompts.lovable.app/ideas/film-animation-animscript-pin-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimScript Pin" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin scripts and screenplays as JSON manifests on IPFS for verifiable, collaborative storytelling. Discipline: Filmmaking & Animation (script integration). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT secures permanent, decentralized text and JSON hosting for script files. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimScriptPin.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAnimScriptPin /// @notice Pin scripts and screenplays as JSON manifests on IPFS for verifiable, collaborative storytelling. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAnimScriptPin { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimScriptPin` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimScriptPin.sol AnimScriptPin` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a script integration, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animshot-library-20 · film-animation · AnimShot Library ===== Pitch: Pin shot lists and planning JSON manifests on IPFS to ensure permanent, tamper-proof records. Page: https://arcprompts.lovable.app/ideas/film-animation-animshot-library-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimShot Library" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin shot lists and planning JSON manifests on IPFS to ensure permanent, tamper-proof records. Discipline: Filmmaking & Animation (shot list archival). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT provides secure, decentralized shot metadata storage immutable over time. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimShotLibrary.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAnimShotLibrary /// @notice Pin shot lists and planning JSON manifests on IPFS to ensure permanent, tamper-proof records. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAnimShotLibrary { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimShotLibrary` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimShotLibrary.sol AnimShotLibrary` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a shot list archival, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animsketch-token-21 · film-animation · AnimSketch Token ===== Pitch: Mint animator sketchbooks as NFTs to prove original creative process ownership. Page: https://arcprompts.lovable.app/ideas/film-animation-animsketch-token-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimSketch Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint animator sketchbooks as NFTs to prove original creative process ownership. Discipline: Filmmaking & Animation (animator sketchbooks). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 minting archives unique sketches immutably on IPFS with creator identity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimSketchToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title AnimSketchToken /// @notice ERC-721 provenance for: Mint animator sketchbooks as NFTs to prove original creative process ownership. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimSketchToken is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("AnimSketchToken", "ANIMSK") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimSketchToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimSketchToken.sol AnimSketchToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a animator sketchbooks, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animsound-ledger-8 · film-animation · AnimSound Ledger ===== Pitch: Mint sound design elements as NFTs verifying originality and usage permissions. Page: https://arcprompts.lovable.app/ideas/film-animation-animsound-ledger-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimSound Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint sound design elements as NFTs verifying originality and usage permissions. Discipline: Filmmaking & Animation (sound design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Immutable NFT minting ensures sound asset provenance and creator rights. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimSoundLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title AnimSoundLedger /// @notice ERC-721 provenance for: Mint sound design elements as NFTs verifying originality and usage permissions. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimSoundLedger is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("AnimSoundLedger", "ANIMSO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimSoundLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimSoundLedger.sol AnimSoundLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a sound design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animstake-voting-2 · film-animation · AnimStake Voting ===== Pitch: Enable decentralized community voting to fund independent animations using secure onchain ballots. Page: https://arcprompts.lovable.app/ideas/film-animation-animstake-voting-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimStake Voting" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable decentralized community voting to fund independent animations using secure onchain ballots. Discipline: Filmmaking & Animation (crowd animation funding). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts reliably manage voting and fund escrow without trusted intermediaries. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimStakeVoting.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AnimStakeVoting /// @notice Enable decentralized community voting to fund independent animations using secure onchain ballots. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimStakeVoting { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimStakeVoting` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimStakeVoting.sol AnimStakeVoting` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a crowd animation funding action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animtext-token-19 · film-animation · AnimText Token ===== Pitch: Mint animated typography clips as NFTs to secure originality and distribution rights. Page: https://arcprompts.lovable.app/ideas/film-animation-animtext-token-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimText Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint animated typography clips as NFTs to secure originality and distribution rights. Discipline: Filmmaking & Animation (animated typography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 minting ensures unique typographic animation ownership linked 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimTextToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title AnimTextToken /// @notice ERC-721 provenance for: Mint animated typography clips as NFTs to secure originality and distribution rights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimTextToken is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("AnimTextToken", "ANIMTE") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimTextToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimTextToken.sol AnimTextToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a animated typography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animtip-jar-12 · film-animation · AnimTip Jar ===== Pitch: Let fans tip animators instantly and transparently via onchain micro-payment smart contracts. Page: https://arcprompts.lovable.app/ideas/film-animation-animtip-jar-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimTip Jar" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Let fans tip animators instantly and transparently via onchain micro-payment smart contracts. Discipline: Filmmaking & Animation (micro-donations). Onchain primitive: Arc smart contract. Why this primitive: Sepolia enables low-cost, immutable transactions directly supporting creators. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimTipJar.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AnimTipJar /// @notice Let fans tip animators instantly and transparently via onchain micro-payment smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimTipJar { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimTipJar` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimTipJar.sol AnimTipJar` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a micro-donations action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animvoice-ledger-12 · film-animation · AnimVoice Ledger ===== Pitch: Pin voiceover takes and metadata to IPFS to maintain immutable, verifiable audio assets. Page: https://arcprompts.lovable.app/ideas/film-animation-animvoice-ledger-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimVoice Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin voiceover takes and metadata to IPFS to maintain immutable, verifiable audio assets. Discipline: Filmmaking & Animation (voice asset management). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT supports large media pinning and JSON metadata for permanent voice asset storage. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimVoiceLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAnimVoiceLedger /// @notice Pin voiceover takes and metadata to IPFS to maintain immutable, verifiable audio assets. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAnimVoiceLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimVoiceLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimVoiceLedger.sol AnimVoiceLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a voice asset management, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-animvoice-ledger-17 · film-animation · AnimVoice Ledger ===== Pitch: Mint voice acting performances as NFTs proving rights and enabling royalty tracking. Page: https://arcprompts.lovable.app/ideas/film-animation-animvoice-ledger-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AnimVoice Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint voice acting performances as NFTs proving rights and enabling royalty tracking. Discipline: Filmmaking & Animation (voice acting). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens anchor audio with creator identity on blockchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AnimVoiceLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title AnimVoiceLedger /// @notice ERC-721 provenance for: Mint voice acting performances as NFTs proving rights and enabling royalty tracking. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AnimVoiceLedger is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("AnimVoiceLedger", "ANIMVO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AnimVoiceLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AnimVoiceLedger.sol AnimVoiceLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a voice acting, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-charactermint-badge-16 · film-animation · CharacterMint Badge ===== Pitch: Authenticate character design sheets as NFTs to establish creator credit and licensing. Page: https://arcprompts.lovable.app/ideas/film-animation-charactermint-badge-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "CharacterMint Badge" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate character design sheets as NFTs to establish creator credit and licensing. Discipline: Filmmaking & Animation (character design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 minting links unique character assets with original artists on Sepolia. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CharacterMintBadge.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title CharacterMintBadge /// @notice ERC-721 provenance for: Authenticate character design sheets as NFTs to establish creator credit and licensing. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CharacterMintBadge is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("CharacterMintBadge", "CHARAC") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CharacterMintBadge` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CharacterMintBadge.sol CharacterMintBadge` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a character design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-charanim-nft-13 · film-animation · CharAnim NFT ===== Pitch: Mint trademarked animation characters as NFTs to secure and trade intellectual property rights. Page: https://arcprompts.lovable.app/ideas/film-animation-charanim-nft-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "CharAnim NFT" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint trademarked animation characters as NFTs to secure and trade intellectual property rights. Discipline: Filmmaking & Animation (character IP NFT). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts enforce provenance and exclusive ownership of characters. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CharAnimNFT.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title CharAnimNFT /// @notice Mint trademarked animation characters as NFTs to secure and trade intellectual property rights. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CharAnimNFT { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CharAnimNFT` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CharAnimNFT.sol CharAnimNFT` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a character IP NFT action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-clipchain-rights-24 · film-animation · ClipChain Rights ===== Pitch: Manage animation clip rights transparently with onchain wallet transactions and gasless sponsored operations. Page: https://arcprompts.lovable.app/ideas/film-animation-clipchain-rights-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ClipChain Rights" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Manage animation clip rights transparently with onchain wallet transactions and gasless sponsored operations. Discipline: Filmmaking & Animation (clip rights management). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy’s embedded wallets with sponsored tx remove friction from rights transfers and proofs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ClipChainRights.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogClipChainRights /// @notice Manage animation clip rights transparently with onchain wallet transactions and gasless sponsored operations. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogClipChainRights { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ClipChainRights` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ClipChainRights.sol ClipChainRights` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every clip rights management action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-clipchain-share-19 · film-animation · ClipChain Share ===== Pitch: Share animation clips instantly with embedded wallets enabling gasless transfers and social proof. Page: https://arcprompts.lovable.app/ideas/film-animation-clipchain-share-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ClipChain Share" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Share animation clips instantly with embedded wallets enabling gasless transfers and social proof. Discipline: Filmmaking & Animation (clip sharing). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy’s wallet with sponsored tx allows instant clip sharing without requiring user gas payments. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ClipChainShare.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogClipChainShare /// @notice Share animation clips instantly with embedded wallets enabling gasless transfers and social proof. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogClipChainShare { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ClipChainShare` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ClipChainShare.sol ClipChainShare` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every clip sharing action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-colorproof-chain-4 · film-animation · ColorProof Chain ===== Pitch: Mint color grading profiles as NFTs to guarantee creative credits and prevent misuse. Page: https://arcprompts.lovable.app/ideas/film-animation-colorproof-chain-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ColorProof Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint color grading profiles as NFTs to guarantee creative credits and prevent misuse. Discipline: Filmmaking & Animation (color grading). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: On-chain minting anchors unique color schemas with verified creators. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ColorProofChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ColorProofChain /// @notice ERC-721 provenance for: Mint color grading profiles as NFTs to guarantee creative credits and prevent misuse. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ColorProofChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ColorProofChain", "COLORP") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ColorProofChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ColorProofChain.sol ColorProofChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a color grading, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-cuttoken-verify-11 · film-animation · CutToken Verify ===== Pitch: Mint finalized film cuts as NFTs to guarantee ownership and distribution rights. Page: https://arcprompts.lovable.app/ideas/film-animation-cuttoken-verify-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "CutToken Verify" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint finalized film cuts as NFTs to guarantee ownership and distribution rights. Discipline: Filmmaking & Animation (film editing). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens link complex edits immutably with creators on Sepolia. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CutTokenVerify.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title CutTokenVerify /// @notice ERC-721 provenance for: Mint finalized film cuts as NFTs to guarantee ownership and distribution rights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CutTokenVerify is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("CutTokenVerify", "CUTTOK") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CutTokenVerify` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CutTokenVerify.sol CutTokenVerify` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a film editing, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-designtoken-flow-13 · film-animation · DesignToken Flow ===== Pitch: Tokenize animation assets and track their usage flow onchain with gasless user wallet transactions. Page: https://arcprompts.lovable.app/ideas/film-animation-designtoken-flow-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "DesignToken Flow" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Tokenize animation assets and track their usage flow onchain with gasless user wallet transactions. Discipline: Filmmaking & Animation (tokenized asset flow). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy’s social wallet plus sponsored tx makes token flow traceable without user crypto knowledge or fees. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DesignTokenFlow.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogDesignTokenFlow /// @notice Tokenize animation assets and track their usage flow onchain with gasless user wallet transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogDesignTokenFlow { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DesignTokenFlow` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DesignTokenFlow.sol DesignTokenFlow` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every tokenized asset flow action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-frame-rights-3 · film-animation · Frame Rights ===== Pitch: License individual animation frames securely with embedded wallets to track ownership and transactions gas-free. Page: https://arcprompts.lovable.app/ideas/film-animation-frame-rights-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Frame Rights" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT License individual animation frames securely with embedded wallets to track ownership and transactions gas-free. Discipline: Filmmaking & Animation (frame licensing). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets with sponsored tx facilitate easy, transparent rights transfers without user gas cost. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FrameRights.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogFrameRights /// @notice License individual animation frames securely with embedded wallets to track ownership and transactions gas-free. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogFrameRights { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FrameRights` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FrameRights.sol FrameRights` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every frame licensing action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-framechain-10 · film-animation · FrameChain ===== Pitch: Record and verify each frame’s creative contribution onchain via seamless social wallet transactions. Page: https://arcprompts.lovable.app/ideas/film-animation-framechain-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FrameChain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record and verify each frame’s creative contribution onchain via seamless social wallet transactions. Discipline: Filmmaking & Animation (frame-by-frame tracking). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy’s embedded wallet plus sponsored tx solves complex contribution tracking gaslessly. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FrameChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogFrameChain /// @notice Record and verify each frame’s creative contribution onchain via seamless social wallet transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogFrameChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FrameChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FrameChain.sol FrameChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every frame-by-frame tracking action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-framechain-ledger-0 · film-animation · FrameChain Ledger ===== Pitch: Verify animation frames' originality and authorship on an immutable blockchain ledger. Page: https://arcprompts.lovable.app/ideas/film-animation-framechain-ledger-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FrameChain Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Verify animation frames' originality and authorship on an immutable blockchain ledger. Discipline: Filmmaking & Animation (frame provenance). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts provide transparent, tamper-proof recording of frame ownership and modifications. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FrameChainLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title FrameChainLedger /// @notice Verify animation frames' originality and authorship on an immutable blockchain ledger. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FrameChainLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FrameChainLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FrameChainLedger.sol FrameChainLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a frame provenance action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-framechain-legacy-0 · film-animation · FrameChain Legacy ===== Pitch: Securely mint unique storyboard sequences proving original authorship and evolution. Page: https://arcprompts.lovable.app/ideas/film-animation-framechain-legacy-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FrameChain Legacy" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely mint unique storyboard sequences proving original authorship and evolution. Discipline: Filmmaking & Animation (storyboard provenance). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Immutable minting ensures incontestable ownership and version history on IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FrameChainLegacy.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title FrameChainLegacy /// @notice ERC-721 provenance for: Securely mint unique storyboard sequences proving original authorship and evolution. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FrameChainLegacy is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("FrameChainLegacy", "FRAMEC") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FrameChainLegacy` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FrameChainLegacy.sol FrameChainLegacy` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a storyboard provenance, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-frameforge-archive-0 · film-animation · FrameForge Archive ===== Pitch: Securely store and share storyboards as immutable IPFS manifests to simplify team collaboration. Page: https://arcprompts.lovable.app/ideas/film-animation-frameforge-archive-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FrameForge Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely store and share storyboards as immutable IPFS manifests to simplify team collaboration. Discipline: Filmmaking & Animation (storyboard management). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT ensures permanent, decentralized storage of visual storyboards and metadata. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FrameForgeArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentFrameForgeArchive /// @notice Securely store and share storyboards as immutable IPFS manifests to simplify team collaboration. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentFrameForgeArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FrameForgeArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FrameForgeArchive.sol FrameForgeArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a storyboard management, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-licenchain-tracker-22 · film-animation · LicenChain Tracker ===== Pitch: Maintain an immutable audit trail of animation licenses and transfers using smart contracts. Page: https://arcprompts.lovable.app/ideas/film-animation-licenchain-tracker-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LicenChain Tracker" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Maintain an immutable audit trail of animation licenses and transfers using smart contracts. Discipline: Filmmaking & Animation (license audit trail). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide transparent, verifiable histories of license transactions. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LicenChainTracker.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title LicenChainTracker /// @notice Maintain an immutable audit trail of animation licenses and transfers using smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract LicenChainTracker { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LicenChainTracker` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LicenChainTracker.sol LicenChainTracker` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a license audit trail action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-licenselock-5 · film-animation · LicenseLock ===== Pitch: Manage animation asset licenses securely and transparently on a blockchain smart contract. Page: https://arcprompts.lovable.app/ideas/film-animation-licenselock-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LicenseLock" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Manage animation asset licenses securely and transparently on a blockchain smart contract. Discipline: Filmmaking & Animation (license management). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts automate license issuance and expiry with tamper-proof records. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LicenseLock.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title LicenseLock /// @notice Manage animation asset licenses securely and transparently on a blockchain smart contract. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract LicenseLock { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LicenseLock` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LicenseLock.sol LicenseLock` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a license management action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-lipsync-ledger-19 · film-animation · LipSync Ledger ===== Pitch: Pin lipsync timing data to IPFS for permanent, sharable, and verifiable sync assets. Page: https://arcprompts.lovable.app/ideas/film-animation-lipsync-ledger-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LipSync Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin lipsync timing data to IPFS for permanent, sharable, and verifiable sync assets. Discipline: Filmmaking & Animation (lipsync data storage). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT supports JSON pinning for immutable lipsync data storage. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LipSyncLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentLipSyncLedger /// @notice Pin lipsync timing data to IPFS for permanent, sharable, and verifiable sync assets. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentLipSyncLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LipSyncLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LipSyncLedger.sol LipSyncLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a lipsync data storage, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-loop-provenance-5 · film-animation · Loop Provenance ===== Pitch: Pin looped animations on IPFS to provide artists with immutable, reusable animation cycles. Page: https://arcprompts.lovable.app/ideas/film-animation-loop-provenance-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Loop Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin looped animations on IPFS to provide artists with immutable, reusable animation cycles. Discipline: Filmmaking & Animation (animation loop libraries). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT enables permanent hosting and easy retrieval of loop animation assets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LoopProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentLoopProvenance /// @notice Pin looped animations on IPFS to provide artists with immutable, reusable animation cycles. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentLoopProvenance { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LoopProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LoopProvenance.sol LoopProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a animation loop libraries, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-loop-provenance-7 · film-animation · Loop Provenance ===== Pitch: Track creation history and ownership of looped animations using gasless onchain wallets. Page: https://arcprompts.lovable.app/ideas/film-animation-loop-provenance-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Loop Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track creation history and ownership of looped animations using gasless onchain wallets. Discipline: Filmmaking & Animation (loop animation tracking). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet plus sponsored transactions removes friction from provenance tracking and transfers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LoopProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogLoopProvenance /// @notice Track creation history and ownership of looped animations using gasless onchain wallets. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogLoopProvenance { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LoopProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LoopProvenance.sol LoopProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every loop animation tracking action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-loopsync-12 · film-animation · LoopSync ===== Pitch: Enable collaborative editing and playback syncing of loops via gasless wallets and onchain state management. Page: https://arcprompts.lovable.app/ideas/film-animation-loopsync-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LoopSync" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable collaborative editing and playback syncing of loops via gasless wallets and onchain state management. Discipline: Filmmaking & Animation (looped animation synchronization). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet and sponsored tx enable seamless multi-user control with zero gas friction. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LoopSync.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogLoopSync /// @notice Enable collaborative editing and playback syncing of loops via gasless wallets and onchain state management. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogLoopSync { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LoopSync` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LoopSync.sol LoopSync` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every looped animation synchronization action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-metascene-ledger-18 · film-animation · MetaScene Ledger ===== Pitch: Track all metadata changes in animation scenes onchain with gasless wallet updates for full provenance. Page: https://arcprompts.lovable.app/ideas/film-animation-metascene-ledger-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MetaScene Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track all metadata changes in animation scenes onchain with gasless wallet updates for full provenance. Discipline: Filmmaking & Animation (scene metadata tracking). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy’s social wallet and sponsored tx remove gas barriers to continuous onchain metadata logging. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MetaSceneLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogMetaSceneLedger /// @notice Track all metadata changes in animation scenes onchain with gasless wallet updates for full provenance. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogMetaSceneLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MetaSceneLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MetaSceneLedger.sol MetaSceneLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every scene metadata tracking action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-modelpin-archive-17 · film-animation · ModelPin Archive ===== Pitch: Pin 3D model metadata and previews on IPFS for permanent documentation and usage logs. Page: https://arcprompts.lovable.app/ideas/film-animation-modelpin-archive-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ModelPin Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin 3D model metadata and previews on IPFS for permanent documentation and usage logs. Discipline: Filmmaking & Animation (3D model documentation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT handles image and JSON pinning ensuring persistent, verifiable model records. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ModelPinArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentModelPinArchive /// @notice Pin 3D model metadata and previews on IPFS for permanent documentation and usage logs. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentModelPinArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ModelPinArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ModelPinArchive.sol ModelPinArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a 3D model documentation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-moodboard-token-13 · film-animation · MoodBoard Token ===== Pitch: Authenticate concept moodboards as NFTs protecting early creative vision rights. Page: https://arcprompts.lovable.app/ideas/film-animation-moodboard-token-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MoodBoard Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate concept moodboards as NFTs protecting early creative vision rights. Discipline: Filmmaking & Animation (concept art). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 minting ties visual concepts to immutable on-chain records. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MoodBoardToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MoodBoardToken /// @notice ERC-721 provenance for: Authenticate concept moodboards as NFTs protecting early creative vision rights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MoodBoardToken is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MoodBoardToken", "MOODBO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MoodBoardToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MoodBoardToken.sol MoodBoardToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a concept art, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-moodboardchain-3 · film-animation · MoodboardChain ===== Pitch: Create decentralized moodboards pinned to IPFS for collaborative color grading projects. Page: https://arcprompts.lovable.app/ideas/film-animation-moodboardchain-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MoodboardChain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create decentralized moodboards pinned to IPFS for collaborative color grading projects. Discipline: Filmmaking & Animation (color study curation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT ensures moodboards remain accessible and unaltered during creative processes. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MoodboardChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMoodboardChain /// @notice Create decentralized moodboards pinned to IPFS for collaborative color grading projects. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMoodboardChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MoodboardChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MoodboardChain.sol MoodboardChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a color study curation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-motion-share-2 · film-animation · Motion Share ===== Pitch: Share and collect feedback on motion graphics with instant, gasless social wallet interactions. Page: https://arcprompts.lovable.app/ideas/film-animation-motion-share-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Motion Share" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Share and collect feedback on motion graphics with instant, gasless social wallet interactions. Discipline: Filmmaking & Animation (motion design sharing). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy’s Google sign-in and sponsored tx enable frictionless social engagement for creatives. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MotionShare.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogMotionShare /// @notice Share and collect feedback on motion graphics with instant, gasless social wallet interactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogMotionShare { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MotionShare` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MotionShare.sol MotionShare` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every motion design sharing action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-motionmap-vault-11 · film-animation · MotionMap Vault ===== Pitch: Store and pin motion capture data on IPFS ensuring permanent access and tamper-proof records. Page: https://arcprompts.lovable.app/ideas/film-animation-motionmap-vault-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MotionMap Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store and pin motion capture data on IPFS ensuring permanent access and tamper-proof records. Discipline: Filmmaking & Animation (motion capture storage). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT handles large JSON/binary data ensuring decentralized motion data permanence. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MotionMapVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMotionMapVault /// @notice Store and pin motion capture data on IPFS ensuring permanent access and tamper-proof records. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMotionMapVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MotionMapVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MotionMapVault.sol MotionMapVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a motion capture storage, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-motionmark-ledger-3 · film-animation · MotionMark Ledger ===== Pitch: Authenticate motion design clips with NFT provenance for fair distribution and recognition. Page: https://arcprompts.lovable.app/ideas/film-animation-motionmark-ledger-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MotionMark Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate motion design clips with NFT provenance for fair distribution and recognition. Discipline: Filmmaking & Animation (motion design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens create secure, transferable motion design ownership proofs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MotionMarkLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MotionMarkLedger /// @notice ERC-721 provenance for: Authenticate motion design clips with NFT provenance for fair distribution and recognition. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MotionMarkLedger is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MotionMarkLedger", "MOTION") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MotionMarkLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MotionMarkLedger.sol MotionMarkLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a motion design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-motionnft-vault-3 · film-animation · MotionNFT Vault ===== Pitch: Mint and trade unique motion design assets as NFTs with verifiable ownership and licensing. Page: https://arcprompts.lovable.app/ideas/film-animation-motionnft-vault-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MotionNFT Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint and trade unique motion design assets as NFTs with verifiable ownership and licensing. Discipline: Filmmaking & Animation (motion asset NFTs). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enable minting and tracking of unique motion NFTs on a secure blockchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MotionNFTVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title MotionNFTVault /// @notice Mint and trade unique motion design assets as NFTs with verifiable ownership and licensing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MotionNFTVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MotionNFTVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MotionNFTVault.sol MotionNFTVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a motion asset NFTs action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-motiontrack-rewards-23 · film-animation · MotionTrack Rewards ===== Pitch: Reward motion designers instantly with sponsored gasless wallet transactions tied to creative milestones. Page: https://arcprompts.lovable.app/ideas/film-animation-motiontrack-rewards-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MotionTrack Rewards" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Reward motion designers instantly with sponsored gasless wallet transactions tied to creative milestones. Discipline: Filmmaking & Animation (performance incentives). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet plus sponsored tx allow seamless incentives without gas payment delays. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MotionTrackRewards.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogMotionTrackRewards /// @notice Reward motion designers instantly with sponsored gasless wallet transactions tied to creative milestones. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogMotionTrackRewards { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MotionTrackRewards` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MotionTrackRewards.sol MotionTrackRewards` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every performance incentives action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-palettechain-9 · film-animation · PaletteChain ===== Pitch: Pin color palettes on IPFS for permanent, shareable references in collaborative projects. Page: https://arcprompts.lovable.app/ideas/film-animation-palettechain-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PaletteChain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin color palettes on IPFS for permanent, shareable references in collaborative projects. Discipline: Filmmaking & Animation (color palette sharing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT's image and JSON pinning enables trustable, decentralized palette hosting. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PaletteChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPaletteChain /// @notice Pin color palettes on IPFS for permanent, shareable references in collaborative projects. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPaletteChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PaletteChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PaletteChain.sol PaletteChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a color palette sharing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-pitchchain-8 · film-animation · PitchChain ===== Pitch: Securely record and track animation pitch submissions leveraging gasless social wallet authentication. Page: https://arcprompts.lovable.app/ideas/film-animation-pitchchain-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PitchChain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely record and track animation pitch submissions leveraging gasless social wallet authentication. Discipline: Filmmaking & Animation (film pitch verification). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy’s Google sign-in and sponsored tx ensure easy, verified pitch submission without user gas. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PitchChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPitchChain /// @notice Securely record and track animation pitch submissions leveraging gasless social wallet authentication. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPitchChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PitchChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PitchChain.sol PitchChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every film pitch verification action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-propchain-license-18 · film-animation · PropChain License ===== Pitch: Mint rental licenses as NFTs for 3D props enabling secure temporary usage. Page: https://arcprompts.lovable.app/ideas/film-animation-propchain-license-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PropChain License" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint rental licenses as NFTs for 3D props enabling secure temporary usage. Discipline: Filmmaking & Animation (virtual prop rental). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance allows traceable licenses and ownership transfers on Sepolia. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PropChainLicense.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PropChainLicense /// @notice ERC-721 provenance for: Mint rental licenses as NFTs for 3D props enabling secure temporary usage. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PropChainLicense is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PropChainLicense", "PROPCH") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PropChainLicense` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PropChainLicense.sol PropChainLicense` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a virtual prop rental, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-propchain-share-21 · film-animation · PropChain Share ===== Pitch: Collaboratively design and pin prop assets on IPFS with permanent version history. Page: https://arcprompts.lovable.app/ideas/film-animation-propchain-share-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PropChain Share" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Collaboratively design and pin prop assets on IPFS with permanent version history. Discipline: Filmmaking & Animation (prop design collaboration). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT enables immutable pinning supporting collaborative asset provenance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PropChainShare.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPropChainShare /// @notice Collaboratively design and pin prop assets on IPFS with permanent version history. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPropChainShare { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PropChainShare` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PropChainShare.sol PropChainShare` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a prop design collaboration, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-propmint-archive-6 · film-animation · PropMint Archive ===== Pitch: Mint 3D prop assets as NFTs to certify originality and control licensing. Page: https://arcprompts.lovable.app/ideas/film-animation-propmint-archive-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PropMint Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint 3D prop assets as NFTs to certify originality and control licensing. Discipline: Filmmaking & Animation (3D prop modeling). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT linkage to IPFS CIDs provides tamper-proof 3D model provenance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PropMintArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PropMintArchive /// @notice ERC-721 provenance for: Mint 3D prop assets as NFTs to certify originality and control licensing. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PropMintArchive is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PropMintArchive", "PROPMI") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PropMintArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PropMintArchive.sol PropMintArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a 3D prop modeling, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-render-economy-6 · film-animation · Render Economy ===== Pitch: Use gasless social wallets to coordinate and incentivize decentralized animation rendering workers. Page: https://arcprompts.lovable.app/ideas/film-animation-render-economy-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Render Economy" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Use gasless social wallets to coordinate and incentivize decentralized animation rendering workers. Discipline: Filmmaking & Animation (distributed rendering). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social wallet with sponsored tx enables seamless payments without user gas barriers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/RenderEconomy.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogRenderEconomy /// @notice Use gasless social wallets to coordinate and incentivize decentralized animation rendering workers. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogRenderEconomy { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs RenderEconomy` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/RenderEconomy.sol RenderEconomy` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every distributed rendering action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-renderstake-9 · film-animation · RenderStake ===== Pitch: Decentralize animation rendering jobs and pay nodes through secure smart contract escrow. Page: https://arcprompts.lovable.app/ideas/film-animation-renderstake-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "RenderStake" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Decentralize animation rendering jobs and pay nodes through secure smart contract escrow. Discipline: Filmmaking & Animation (distributed rendering). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts automate fair task distribution and payment release upon verification. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/RenderStake.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title RenderStake /// @notice Decentralize animation rendering jobs and pay nodes through secure smart contract escrow. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract RenderStake { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs RenderStake` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/RenderStake.sol RenderStake` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a distributed rendering action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-rigpin-sync-4 · film-animation · RigPin Sync ===== Pitch: Share and pin character rig files and metadata on IPFS for verified reuse across teams. Page: https://arcprompts.lovable.app/ideas/film-animation-rigpin-sync-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "RigPin Sync" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Share and pin character rig files and metadata on IPFS for verified reuse across teams. Discipline: Filmmaking & Animation (character rig sharing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT guarantees persistent hosting of large rig files and version history. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/RigPinSync.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentRigPinSync /// @notice Share and pin character rig files and metadata on IPFS for verified reuse across teams. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentRigPinSync { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs RigPinSync` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/RigPinSync.sol RigPinSync` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a character rig sharing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-sceneaccess-pass-20 · film-animation · SceneAccess Pass ===== Pitch: Mint NFT passes granting creator access to virtual production scene data and assets. Page: https://arcprompts.lovable.app/ideas/film-animation-sceneaccess-pass-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SceneAccess Pass" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFT passes granting creator access to virtual production scene data and assets. Discipline: Filmmaking & Animation (virtual production). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens securely regulate and verify access rights on Sepolia. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SceneAccessPass.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SceneAccessPass /// @notice ERC-721 provenance for: Mint NFT passes granting creator access to virtual production scene data and assets. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SceneAccessPass is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SceneAccessPass", "SCENEA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SceneAccessPass` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SceneAccessPass.sol SceneAccessPass` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a virtual production, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-scenemetastore-6 · film-animation · SceneMetaStore ===== Pitch: Attach and pin rich JSON metadata to animation scenes, ensuring permanent scene context storage. Page: https://arcprompts.lovable.app/ideas/film-animation-scenemetastore-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SceneMetaStore" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Attach and pin rich JSON metadata to animation scenes, ensuring permanent scene context storage. Discipline: Filmmaking & Animation (metadata tagging). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT's JSON pinning secures metadata's immutability and accessibility forever. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SceneMetaStore.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentSceneMetaStore /// @notice Attach and pin rich JSON metadata to animation scenes, ensuring permanent scene context storage. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentSceneMetaStore { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SceneMetaStore` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SceneMetaStore.sol SceneMetaStore` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a metadata tagging, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-scenemood-chain-18 · film-animation · SceneMood Chain ===== Pitch: Record color grading versions on blockchain to secure post-production creative decisions. Page: https://arcprompts.lovable.app/ideas/film-animation-scenemood-chain-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SceneMood Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record color grading versions on blockchain to secure post-production creative decisions. Discipline: Filmmaking & Animation (color grading proof). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts timestamp and preserve immutable color study records. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SceneMoodChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SceneMoodChain /// @notice Record color grading versions on blockchain to secure post-production creative decisions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SceneMoodChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SceneMoodChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SceneMoodChain.sol SceneMoodChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a color grading proof action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-scenestamp-auth-2 · film-animation · SceneStamp Auth ===== Pitch: Mint entire scene compositions as NFTs to protect creative framing and direction. Page: https://arcprompts.lovable.app/ideas/film-animation-scenestamp-auth-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SceneStamp Auth" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint entire scene compositions as NFTs to protect creative framing and direction. Discipline: Filmmaking & Animation (scene composition). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance solidifies scene ownership and timestamp on Sepolia. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SceneStampAuth.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SceneStampAuth /// @notice ERC-721 provenance for: Mint entire scene compositions as NFTs to protect creative framing and direction. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SceneStampAuth is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SceneStampAuth", "SCENES") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SceneStampAuth` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SceneStampAuth.sol SceneStampAuth` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a scene composition, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-sceneswap-dex-7 · film-animation · SceneSwap DEX ===== Pitch: Trade and swap 3D scenes and animation assets peer-to-peer with secure onchain escrow. Page: https://arcprompts.lovable.app/ideas/film-animation-sceneswap-dex-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SceneSwap DEX" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Trade and swap 3D scenes and animation assets peer-to-peer with secure onchain escrow. Discipline: Filmmaking & Animation (asset exchange). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide trustless atomic swaps and automated payments for digital assets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SceneSwapDEX.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SceneSwapDEX /// @notice Trade and swap 3D scenes and animation assets peer-to-peer with secure onchain escrow. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SceneSwapDEX { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SceneSwapDEX` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SceneSwapDEX.sol SceneSwapDEX` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a asset exchange action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-scriptmint-1 · film-animation · ScriptMint ===== Pitch: Track and transfer screenplay rights securely through blockchain for transparent ownership history. Page: https://arcprompts.lovable.app/ideas/film-animation-scriptmint-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptMint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track and transfer screenplay rights securely through blockchain for transparent ownership history. Discipline: Filmmaking & Animation (script rights tracking). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts automate rights transfer and record it immutably on Sepolia. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ScriptMint /// @notice Track and transfer screenplay rights securely through blockchain for transparent ownership history. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ScriptMint { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptMint.sol ScriptMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a script rights tracking action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-storyboard-captioner-22 · film-animation · Storyboard Captioner ===== Pitch: Pin dialogue captions linked to storyboard frames on IPFS for permanent dialogue-frame sync. Page: https://arcprompts.lovable.app/ideas/film-animation-storyboard-captioner-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Storyboard Captioner" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin dialogue captions linked to storyboard frames on IPFS for permanent dialogue-frame sync. Discipline: Filmmaking & Animation (dialogue annotation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT's JSON pinning offers decentralized, permanent dialogue annotation storage. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StoryboardCaptioner.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentStoryboardCaptioner /// @notice Pin dialogue captions linked to storyboard frames on IPFS for permanent dialogue-frame sync. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentStoryboardCaptioner { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StoryboardCaptioner` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StoryboardCaptioner.sol StoryboardCaptioner` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a dialogue annotation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-storyboard-chain-9 · film-animation · Storyboard Chain ===== Pitch: Secure visual storyboards on-chain to track creator rights and version history. Page: https://arcprompts.lovable.app/ideas/film-animation-storyboard-chain-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Storyboard Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Secure visual storyboards on-chain to track creator rights and version history. Discipline: Filmmaking & Animation (visual storytelling). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 minting with IPFS CIDs provides immutable storyboard evidence. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StoryboardChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title StoryboardChain /// @notice ERC-721 provenance for: Secure visual storyboards on-chain to track creator rights and version history. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StoryboardChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("StoryboardChain", "STORYB") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StoryboardChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StoryboardChain.sol StoryboardChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a visual storytelling, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-storyboard-mint-21 · film-animation · Storyboard Mint ===== Pitch: Mint storyboards as gasless NFTs via embedded social wallets for easy ownership and transfer. Page: https://arcprompts.lovable.app/ideas/film-animation-storyboard-mint-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Storyboard Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint storyboards as gasless NFTs via embedded social wallets for easy ownership and transfer. Discipline: Filmmaking & Animation (storyboard NFT minting). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets plus sponsored transactions remove minting gas barriers for creators and producers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StoryboardMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogStoryboardMint /// @notice Mint storyboards as gasless NFTs via embedded social wallets for easy ownership and transfer. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogStoryboardMint { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StoryboardMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StoryboardMint.sol StoryboardMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every storyboard NFT minting action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-storyboard-replay-16 · film-animation · Storyboard Replay ===== Pitch: Pin animatic sequences and frames on IPFS for permanent, shareable playback references. Page: https://arcprompts.lovable.app/ideas/film-animation-storyboard-replay-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Storyboard Replay" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin animatic sequences and frames on IPFS for permanent, shareable playback references. Discipline: Filmmaking & Animation (animatic playback). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT enables permanent storage of image sequences and JSON timing manifests. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StoryboardReplay.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentStoryboardReplay /// @notice Pin animatic sequences and frames on IPFS for permanent, shareable playback references. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentStoryboardReplay { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StoryboardReplay` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StoryboardReplay.sol StoryboardReplay` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a animatic playback, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-storyboard-sync-0 · film-animation · Storyboard Sync ===== Pitch: Enable real-time, gasless syncing and feedback on storyboards among filmmakers and artists. Page: https://arcprompts.lovable.app/ideas/film-animation-storyboard-sync-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Storyboard Sync" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable real-time, gasless syncing and feedback on storyboards among filmmakers and artists. Discipline: Filmmaking & Animation (storyboard collaboration). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy’s social wallet plus sponsored tx allows seamless, user-friendly collaborative edits without blockchain friction. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StoryboardSync.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogStoryboardSync /// @notice Enable real-time, gasless syncing and feedback on storyboards among filmmakers and artists. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogStoryboardSync { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StoryboardSync` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StoryboardSync.sol StoryboardSync` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every storyboard collaboration action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-storyboardchain-4 · film-animation · StoryboardChain ===== Pitch: Record storyboard versions onchain to prove originality and evolution of narrative visuals. Page: https://arcprompts.lovable.app/ideas/film-animation-storyboardchain-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StoryboardChain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record storyboard versions onchain to prove originality and evolution of narrative visuals. Discipline: Filmmaking & Animation (storyboard authenticity). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts immutably track and timestamp artwork revisions securely. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StoryboardChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title StoryboardChain /// @notice Record storyboard versions onchain to prove originality and evolution of narrative visuals. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StoryboardChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StoryboardChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StoryboardChain.sol StoryboardChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a storyboard authenticity action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-storyboardchain-7 · film-animation · StoryboardChain ===== Pitch: Pin storyboards and scripts to IPFS for tamper-proof, collaborative episodic planning. Page: https://arcprompts.lovable.app/ideas/film-animation-storyboardchain-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StoryboardChain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin storyboards and scripts to IPFS for tamper-proof, collaborative episodic planning. Discipline: Filmmaking & Animation (distributed storyboarding). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT secures permanent, decentralized storage of evolving creative documents. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StoryboardChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentStoryboardChain /// @notice Pin storyboards and scripts to IPFS for tamper-proof, collaborative episodic planning. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentStoryboardChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StoryboardChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StoryboardChain.sol StoryboardChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a distributed storyboarding, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-storyboardchain-script-12 · film-animation · StoryboardChain Script ===== Pitch: Mint script drafts as NFTs to secure writing credits and version control. Page: https://arcprompts.lovable.app/ideas/film-animation-storyboardchain-script-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StoryboardChain Script" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint script drafts as NFTs to secure writing credits and version control. Discipline: Filmmaking & Animation (script development). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance on IPFS timestamps and validates original script ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StoryboardChainScript.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title StoryboardChainScript /// @notice ERC-721 provenance for: Mint script drafts as NFTs to secure writing credits and version control. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StoryboardChainScript is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("StoryboardChainScript", "STORYB") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StoryboardChainScript` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StoryboardChainScript.sol StoryboardChainScript` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a script development, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-storyboardclip-token-15 · film-animation · StoryboardClip Token ===== Pitch: Mint animatic clips as NFTs to secure early-stage animated story ownership. Page: https://arcprompts.lovable.app/ideas/film-animation-storyboardclip-token-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StoryboardClip Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint animatic clips as NFTs to secure early-stage animated story ownership. Discipline: Filmmaking & Animation (animatic creation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: On-chain provenance verifies combined storyboard and timing clips. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StoryboardClipToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title StoryboardClipToken /// @notice ERC-721 provenance for: Mint animatic clips as NFTs to secure early-stage animated story ownership. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StoryboardClipToken is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("StoryboardClipToken", "STORYB") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StoryboardClipToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StoryboardClipToken.sol StoryboardClipToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a animatic creation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-storyboardsync-ledger-22 · film-animation · StoryboardSync Ledger ===== Pitch: Mint NFT storyboards to track collaborative edits and contributions transparently. Page: https://arcprompts.lovable.app/ideas/film-animation-storyboardsync-ledger-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StoryboardSync Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFT storyboards to track collaborative edits and contributions transparently. Discipline: Filmmaking & Animation (collaborative storyboarding). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens provide auditable provenance on multi-contributor storyboards. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StoryboardSyncLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title StoryboardSyncLedger /// @notice ERC-721 provenance for: Mint NFT storyboards to track collaborative edits and contributions transparently. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StoryboardSyncLedger is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("StoryboardSyncLedger", "STORYB") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StoryboardSyncLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StoryboardSyncLedger.sol StoryboardSyncLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a collaborative storyboarding, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-styleswap-15 · film-animation · StyleSwap ===== Pitch: Exchange and remix animation styles with gasless wallet onboarding and instant transaction sponsorship. Page: https://arcprompts.lovable.app/ideas/film-animation-styleswap-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StyleSwap" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Exchange and remix animation styles with gasless wallet onboarding and instant transaction sponsorship. Discipline: Filmmaking & Animation (animation style exchange). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy’s embedded wallet ensures frictionless user experience with zero gas during style swaps. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StyleSwap.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogStyleSwap /// @notice Exchange and remix animation styles with gasless wallet onboarding and instant transaction sponsorship. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogStyleSwap { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StyleSwap` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StyleSwap.sol StyleSwap` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every animation style exchange action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-texturevault-1 · film-animation · TextureVault ===== Pitch: Pin and catalog textures on IPFS for reuse and verified provenance in animation projects. Page: https://arcprompts.lovable.app/ideas/film-animation-texturevault-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "TextureVault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin and catalog textures on IPFS for reuse and verified provenance in animation projects. Discipline: Filmmaking & Animation (material asset library). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT gives permanent decentralized image hosting, enabling trustworthy asset reuse. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TextureVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentTextureVault /// @notice Pin and catalog textures on IPFS for reuse and verified provenance in animation projects. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentTextureVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TextureVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TextureVault.sol TextureVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a material asset library, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-vfx-provenance-5 · film-animation · VFX Provenance ===== Pitch: Secure VFX shots as NFTs ensuring rightful ownership and usage tracking across productions. Page: https://arcprompts.lovable.app/ideas/film-animation-vfx-provenance-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VFX Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Secure VFX shots as NFTs ensuring rightful ownership and usage tracking across productions. Discipline: Filmmaking & Animation (visual effects). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 minting allows immutable proof of complex effect ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VFXProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title VFXProvenance /// @notice ERC-721 provenance for: Secure VFX shots as NFTs ensuring rightful ownership and usage tracking across productions. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VFXProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("VFXProvenance", "VFXPRO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VFXProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VFXProvenance.sol VFXProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a visual effects, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-vfxchain-sync-13 · film-animation · VFXChain Sync ===== Pitch: Pin VFX layers and settings on IPFS for permanent, sharable effects asset libraries. Page: https://arcprompts.lovable.app/ideas/film-animation-vfxchain-sync-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VFXChain Sync" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin VFX layers and settings on IPFS for permanent, sharable effects asset libraries. Discipline: Filmmaking & Animation (effects asset curation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT allows immutable storage of complex JSON manifests representing effects layers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VFXChainSync.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentVFXChainSync /// @notice Pin VFX layers and settings on IPFS for permanent, sharable effects asset libraries. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentVFXChainSync { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VFXChainSync` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VFXChainSync.sol VFXChainSync` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a effects asset curation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-voice-sync-4 · film-animation · Voice Sync ===== Pitch: Sync and verify voice-over sessions onchain with social wallets for smooth, gasless collaboration. Page: https://arcprompts.lovable.app/ideas/film-animation-voice-sync-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Voice Sync" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Sync and verify voice-over sessions onchain with social wallets for smooth, gasless collaboration. Discipline: Filmmaking & Animation (voice-over integration). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy’s social wallet and sponsored transactions streamline voice sync without blockchain UX headaches. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VoiceSync.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogVoiceSync /// @notice Sync and verify voice-over sessions onchain with social wallets for smooth, gasless collaboration. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogVoiceSync { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VoiceSync` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VoiceSync.sol VoiceSync` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every voice-over integration action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-voicechain-cast-16 · film-animation · VoiceChain Cast ===== Pitch: Track voice actor rights and usage in animation projects via gasless social wallet authentication. Page: https://arcprompts.lovable.app/ideas/film-animation-voicechain-cast-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VoiceChain Cast" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track voice actor rights and usage in animation projects via gasless social wallet authentication. Discipline: Filmmaking & Animation (cast rights tracking). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy’s social wallet plus sponsored tx delivers seamless rights management without blockchain complexity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VoiceChainCast.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogVoiceChainCast /// @notice Track voice actor rights and usage in animation projects via gasless social wallet authentication. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogVoiceChainCast { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VoiceChainCast` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VoiceChainCast.sol VoiceChainCast` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every cast rights tracking action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-voicechain-sync-11 · film-animation · VoiceChain Sync ===== Pitch: Securely synchronize and timestamp voice acting tracks with animation frames on blockchain. Page: https://arcprompts.lovable.app/ideas/film-animation-voicechain-sync-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VoiceChain Sync" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely synchronize and timestamp voice acting tracks with animation frames on blockchain. Discipline: Filmmaking & Animation (voice sync verification). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts ensure immutable matching records of audio and frame data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VoiceChainSync.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title VoiceChainSync /// @notice Securely synchronize and timestamp voice acting tracks with animation frames on blockchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VoiceChainSync { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VoiceChainSync` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VoiceChainSync.sol VoiceChainSync` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a voice sync verification action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA film-animation-voicetrack-token-7 · film-animation · VoiceTrack Token ===== Pitch: Authenticate voiceover clips with provenance NFTs ensuring creator recognition and rights. Page: https://arcprompts.lovable.app/ideas/film-animation-voicetrack-token-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VoiceTrack Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate voiceover clips with provenance NFTs ensuring creator recognition and rights. Discipline: Filmmaking & Animation (voiceover recording). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens anchor unique audio assets on-chain linked to creators. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VoiceTrackToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title VoiceTrackToken /// @notice ERC-721 provenance for: Authenticate voiceover clips with provenance NFTs ensuring creator recognition and rights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VoiceTrackToken is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("VoiceTrackToken", "VOICET") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VoiceTrackToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VoiceTrackToken.sol VoiceTrackToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a voiceover recording, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-ai-art-provenance-17 · games · AI Art Provenance ===== Pitch: Mint AI-generated game art NFTs with provenance proving creator guidance originality. Page: https://arcprompts.lovable.app/ideas/games-ai-art-provenance-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AI Art Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint AI-generated game art NFTs with provenance proving creator guidance originality. Discipline: Game Design & Interactive Media (AI-generated game art). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Provenance ensures proper attribution of AI-assisted art via NFT minting. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AIArtProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title AIArtProvenance /// @notice ERC-721 provenance for: Mint AI-generated game art NFTs with provenance proving creator guidance originality. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AIArtProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("AIArtProvenance", "AIARTP") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AIArtProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AIArtProvenance.sol AIArtProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a AI-generated game art, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-ai-npc-memory-hub-8 · games · AI NPC Memory Hub ===== Pitch: Store AI NPC dialogue and memory states permanently for persistent, evolving game worlds. Page: https://arcprompts.lovable.app/ideas/games-ai-npc-memory-hub-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AI NPC Memory Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store AI NPC dialogue and memory states permanently for persistent, evolving game worlds. Discipline: Game Design & Interactive Media (non-player character data). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata IPFS ensures NPC data persistence, enabling decentralized world state consistency. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AINPCMemoryHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAINPCMemoryHub /// @notice Store AI NPC dialogue and memory states permanently for persistent, evolving game worlds. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAINPCMemoryHub { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AINPCMemoryHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AINPCMemoryHub.sol AINPCMemoryHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a non-player character data, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-ar-filter-provenance-12 · games · AR Filter Provenance ===== Pitch: Create and mint AR filters as NFTs ensuring verified creator ownership. Page: https://arcprompts.lovable.app/ideas/games-ar-filter-provenance-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AR Filter Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create and mint AR filters as NFTs ensuring verified creator ownership. Discipline: Game Design & Interactive Media (augmented reality effects). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Provenance minting secures original AR visual effect authorship onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ARFilterProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ARFilterProvenance /// @notice ERC-721 provenance for: Create and mint AR filters as NFTs ensuring verified creator ownership. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ARFilterProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ARFilterProvenance", "ARFILT") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ARFilterProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ARFilterProvenance.sol ARFilterProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a augmented reality effects, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-artifact-provenance-ledger-5 · games · Artifact Provenance Ledger ===== Pitch: Track and preserve full history of rare game artifacts immutably for authentic ownership proof. Page: https://arcprompts.lovable.app/ideas/games-artifact-provenance-ledger-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Artifact Provenance Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track and preserve full history of rare game artifacts immutably for authentic ownership proof. Discipline: Game Design & Interactive Media (in-game item history tracking). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinning JSON manifests to IPFS ensures permanent, trustless provenance data storage. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ArtifactProvenanceLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentArtifactProvenanceLedger /// @notice Track and preserve full history of rare game artifacts immutably for authentic ownership proof. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentArtifactProvenanceLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ArtifactProvenanceLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ArtifactProvenanceLedger.sol ArtifactProvenanceLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a in-game item history tracking, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-audio-loop-library-6 · games · Audio Loop Library ===== Pitch: Curate and permanently store unique game audio loops for easy reuse and remixing. Page: https://arcprompts.lovable.app/ideas/games-audio-loop-library-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Audio Loop Library" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Curate and permanently store unique game audio loops for easy reuse and remixing. Discipline: Game Design & Interactive Media (sound asset curation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS storage via Pinata preserves immutable high-quality audio assets accessible globally. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AudioLoopLibrary.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAudioLoopLibrary /// @notice Curate and permanently store unique game audio loops for easy reuse and remixing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAudioLoopLibrary { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AudioLoopLibrary` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AudioLoopLibrary.sol AudioLoopLibrary` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a sound asset curation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-avatar-dna-chain-4 · games · Avatar DNA Chain ===== Pitch: Create unique avatars with provenance-tracked traits minted as ERC-721 tokens. Page: https://arcprompts.lovable.app/ideas/games-avatar-dna-chain-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Avatar DNA Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create unique avatars with provenance-tracked traits minted as ERC-721 tokens. Discipline: Game Design & Interactive Media (custom avatar creation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT minting confirms original avatar trait ownership and prevents copying. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AvatarDNAChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title AvatarDNAChain /// @notice ERC-721 provenance for: Create unique avatars with provenance-tracked traits minted as ERC-721 tokens. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AvatarDNAChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("AvatarDNAChain", "AVATAR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AvatarDNAChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AvatarDNAChain.sol AvatarDNAChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a custom avatar creation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-avatar-trait-forge-1 · games · Avatar Trait Forge ===== Pitch: Save and share unique character traits and skins securely and permanently on IPFS. Page: https://arcprompts.lovable.app/ideas/games-avatar-trait-forge-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Avatar Trait Forge" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Save and share unique character traits and skins securely and permanently on IPFS. Discipline: Game Design & Interactive Media (character customization data). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinning character assets to IPFS guarantees permanent player ownership and access. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AvatarTraitForge.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAvatarTraitForge /// @notice Save and share unique character traits and skins securely and permanently on IPFS. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAvatarTraitForge { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AvatarTraitForge` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AvatarTraitForge.sol AvatarTraitForge` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a character customization data, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-blockchain-dialogue-trees-20 · games · Blockchain Dialogue Trees ===== Pitch: Store branching dialogue choices on Sepolia to enable transparent player-driven storytelling. Page: https://arcprompts.lovable.app/ideas/games-blockchain-dialogue-trees-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Blockchain Dialogue Trees" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store branching dialogue choices on Sepolia to enable transparent player-driven storytelling. Discipline: Game Design & Interactive Media (interactive dialogue). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts maintain an immutable record of dialogue paths taken. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/BlockchainDialogueTrees.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title BlockchainDialogueTrees /// @notice Store branching dialogue choices on Sepolia to enable transparent player-driven storytelling. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract BlockchainDialogueTrees { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs BlockchainDialogueTrees` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/BlockchainDialogueTrees.sol BlockchainDialogueTrees` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a interactive dialogue action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-collectible-avatars-5 · games · Collectible Avatars ===== Pitch: Mint avatar parts as unique tokens on Sepolia, enabling player-owned, tradable character features. Page: https://arcprompts.lovable.app/ideas/games-collectible-avatars-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Collectible Avatars" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint avatar parts as unique tokens on Sepolia, enabling player-owned, tradable character features. Discipline: Game Design & Interactive Media (character customization). Onchain primitive: Arc smart contract. Why this primitive: Sepolia supports unique ownership and provenance tracking for avatar elements. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CollectibleAvatars.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title CollectibleAvatars /// @notice Mint avatar parts as unique tokens on Sepolia, enabling player-owned, tradable character features. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CollectibleAvatars { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CollectibleAvatars` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CollectibleAvatars.sol CollectibleAvatars` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a character customization action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-collectible-lore-cards-22 · games · Collectible Lore Cards ===== Pitch: Mint unique lore cards as NFTs proving authentic creator provenance. Page: https://arcprompts.lovable.app/ideas/games-collectible-lore-cards-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Collectible Lore Cards" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint unique lore cards as NFTs proving authentic creator provenance. Discipline: Game Design & Interactive Media (digital collectible design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Provenance minting ensures collectible authenticity and creator ownership onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CollectibleLoreCards.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title CollectibleLoreCards /// @notice ERC-721 provenance for: Mint unique lore cards as NFTs proving authentic creator provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CollectibleLoreCards is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("CollectibleLoreCards", "COLLEC") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CollectibleLoreCards` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CollectibleLoreCards.sol CollectibleLoreCards` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a digital collectible design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-decentralized-game-mods-22 · games · Decentralized Game Mods ===== Pitch: Distribute and verify mods using Sepolia smart contracts for secure, community-driven content sharing. Page: https://arcprompts.lovable.app/ideas/games-decentralized-game-mods-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Decentralized Game Mods" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Distribute and verify mods using Sepolia smart contracts for secure, community-driven content sharing. Discipline: Game Design & Interactive Media (mod distribution). Onchain primitive: Arc smart contract. Why this primitive: Sepolia enables verifiable ownership and integrity checks for mod files. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DecentralizedGameMods.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DecentralizedGameMods /// @notice Distribute and verify mods using Sepolia smart contracts for secure, community-driven content sharing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DecentralizedGameMods { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DecentralizedGameMods` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DecentralizedGameMods.sol DecentralizedGameMods` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a mod distribution action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-decentralized-game-voting-7 · games · Decentralized Game Voting ===== Pitch: Empower players to vote on game decisions using Sepolia smart contracts for transparent results. Page: https://arcprompts.lovable.app/ideas/games-decentralized-game-voting-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Decentralized Game Voting" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Empower players to vote on game decisions using Sepolia smart contracts for transparent results. Discipline: Game Design & Interactive Media (community governance). Onchain primitive: Arc smart contract. Why this primitive: Sepolia ensures secure, verifiable voting with onchain tallying and auditability. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DecentralizedGameVoting.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DecentralizedGameVoting /// @notice Empower players to vote on game decisions using Sepolia smart contracts for transparent results. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DecentralizedGameVoting { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DecentralizedGameVoting` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DecentralizedGameVoting.sol DecentralizedGameVoting` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a community governance action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-decentralized-story-co-creation-16 · games · Decentralized Story Co-Creation ===== Pitch: Facilitate community-driven story creation by recording contributions on Sepolia for transparent authorship. Page: https://arcprompts.lovable.app/ideas/games-decentralized-story-co-creation-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Decentralized Story Co-Creation" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Facilitate community-driven story creation by recording contributions on Sepolia for transparent authorship. Discipline: Game Design & Interactive Media (collaborative narrative). Onchain primitive: Arc smart contract. Why this primitive: Sepolia enables tamper-proof tracking of collaborative input and ownership rights. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DecentralizedStoryCoCreation.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DecentralizedStoryCoCreation /// @notice Facilitate community-driven story creation by recording contributions on Sepolia for transparent authorship. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DecentralizedStoryCoCreation { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DecentralizedStoryCoCreation` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DecentralizedStoryCoCreation.sol DecentralizedStoryCoCreation` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a collaborative narrative action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-dynamic-nft-game-assets-12 · games · Dynamic NFT Game Assets ===== Pitch: Allow game assets to evolve on Sepolia based on gameplay, creating unique player-owned NFTs. Page: https://arcprompts.lovable.app/ideas/games-dynamic-nft-game-assets-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dynamic NFT Game Assets" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Allow game assets to evolve on Sepolia based on gameplay, creating unique player-owned NFTs. Discipline: Game Design & Interactive Media (asset evolution). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts enable programmable, state-changing blockchain assets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DynamicNFTGameAssets.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DynamicNFTGameAssets /// @notice Allow game assets to evolve on Sepolia based on gameplay, creating unique player-owned NFTs. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DynamicNFTGameAssets { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DynamicNFTGameAssets` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DynamicNFTGameAssets.sol DynamicNFTGameAssets` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a asset evolution action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-dynamic-npc-tokens-15 · games · Dynamic NPC Tokens ===== Pitch: Mint dynamic NPCs as NFTs with provable creator-generated traits and backstories. Page: https://arcprompts.lovable.app/ideas/games-dynamic-npc-tokens-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dynamic NPC Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint dynamic NPCs as NFTs with provable creator-generated traits and backstories. Discipline: Game Design & Interactive Media (procedural character design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance links procedural NPC data securely to original creators. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DynamicNPCTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title DynamicNPCTokens /// @notice ERC-721 provenance for: Mint dynamic NPCs as NFTs with provable creator-generated traits and backstories. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DynamicNPCTokens is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("DynamicNPCTokens", "DYNAMI") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DynamicNPCTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DynamicNPCTokens.sol DynamicNPCTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a procedural character design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-dynamic-ui-blueprint-11 · games · Dynamic UI Blueprint ===== Pitch: Save and distribute dynamic game UI states and settings with permanent accessibility. Page: https://arcprompts.lovable.app/ideas/games-dynamic-ui-blueprint-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dynamic UI Blueprint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Save and distribute dynamic game UI states and settings with permanent accessibility. Discipline: Game Design & Interactive Media (interface state storage). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinning JSON UI states to IPFS ensures seamless cross-device interface consistency. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DynamicUIBlueprint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentDynamicUIBlueprint /// @notice Save and distribute dynamic game UI states and settings with permanent accessibility. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentDynamicUIBlueprint { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DynamicUIBlueprint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DynamicUIBlueprint.sol DynamicUIBlueprint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a interface state storage, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-emotion-filter-pack-16 · games · Emotion Filter Pack ===== Pitch: Pin and share unique emotion-driven visual filters permanently for character and scene enhancement. Page: https://arcprompts.lovable.app/ideas/games-emotion-filter-pack-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Emotion Filter Pack" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin and share unique emotion-driven visual filters permanently for character and scene enhancement. Discipline: Game Design & Interactive Media (visual effect assets). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Permanent IPFS storage ensures visual effects remain accessible despite platform changes. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EmotionFilterPack.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentEmotionFilterPack /// @notice Pin and share unique emotion-driven visual filters permanently for character and scene enhancement. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentEmotionFilterPack { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EmotionFilterPack` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EmotionFilterPack.sol EmotionFilterPack` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a visual effect assets, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-game-font-provenance-24 · games · Game Font Provenance ===== Pitch: Mint unique game fonts as NFTs proving original designer ownership. Page: https://arcprompts.lovable.app/ideas/games-game-font-provenance-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Game Font Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint unique game fonts as NFTs proving original designer ownership. Discipline: Game Design & Interactive Media (typography for games). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Provenance minting authenticates font designs linked on IPFS to creators. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GameFontProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title GameFontProvenance /// @notice ERC-721 provenance for: Mint unique game fonts as NFTs proving original designer ownership. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract GameFontProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("GameFontProvenance", "GAMEFO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GameFontProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GameFontProvenance.sol GameFontProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a typography for games, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-game-jam-showcase-23 · games · Game Jam Showcase ===== Pitch: Permanently pin game jam project assets and manifests to showcase creative works indefinitely. Page: https://arcprompts.lovable.app/ideas/games-game-jam-showcase-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Game Jam Showcase" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Permanently pin game jam project assets and manifests to showcase creative works indefinitely. Discipline: Game Design & Interactive Media (project preservation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS preserves ephemeral jam projects beyond event timelines reliably and openly. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GameJamShowcase.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentGameJamShowcase /// @notice Permanently pin game jam project assets and manifests to showcase creative works indefinitely. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentGameJamShowcase { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GameJamShowcase` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GameJamShowcase.sol GameJamShowcase` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a project preservation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-game-mod-provenance-7 · games · Game Mod Provenance ===== Pitch: Mint unique game mods with provenance to authenticate original creator contributions. Page: https://arcprompts.lovable.app/ideas/games-game-mod-provenance-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Game Mod Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint unique game mods with provenance to authenticate original creator contributions. Discipline: Game Design & Interactive Media (modding community tools). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain provenance prevents unauthorized mod copying and credits creators. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GameModProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title GameModProvenance /// @notice ERC-721 provenance for: Mint unique game mods with provenance to authenticate original creator contributions. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract GameModProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("GameModProvenance", "GAMEMO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GameModProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GameModProvenance.sol GameModProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a modding community tools, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-gas-free-co-op-play-13 · games · Gas-Free Co-op Play ===== Pitch: Join cooperative matches instantly with gasless wallet onboarding and sponsored sync transactions. Page: https://arcprompts.lovable.app/ideas/games-gas-free-co-op-play-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gas-Free Co-op Play" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Join cooperative matches instantly with gasless wallet onboarding and sponsored sync transactions. Discipline: Game Design & Interactive Media (multiplayer mechanics). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet plus sponsored tx minimizes gas barriers for multiplayer joining. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GasFreeCoopPlay.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGasFreeCoopPlay /// @notice Join cooperative matches instantly with gasless wallet onboarding and sponsored sync transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGasFreeCoopPlay { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GasFreeCoopPlay` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GasFreeCoopPlay.sol GasFreeCoopPlay` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every multiplayer mechanics action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-gas-free-level-sharing-22 · games · Gas-Free Level Sharing ===== Pitch: Share custom levels with friends through gasless wallet sign-in and sponsored access transactions. Page: https://arcprompts.lovable.app/ideas/games-gas-free-level-sharing-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gas-Free Level Sharing" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Share custom levels with friends through gasless wallet sign-in and sponsored access transactions. Discipline: Game Design & Interactive Media (user content). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet and sponsored tx enable easy, gasless content sharing. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GasFreeLevelSharing.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGasFreeLevelSharing /// @notice Share custom levels with friends through gasless wallet sign-in and sponsored access transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGasFreeLevelSharing { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GasFreeLevelSharing` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GasFreeLevelSharing.sol GasFreeLevelSharing` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every user content action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-gasless-avatar-battles-16 · games · Gasless Avatar Battles ===== Pitch: Compete in avatar battles with gas-free wallet authentication and sponsored match transactions. Page: https://arcprompts.lovable.app/ideas/games-gasless-avatar-battles-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Avatar Battles" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Compete in avatar battles with gas-free wallet authentication and sponsored match transactions. Discipline: Game Design & Interactive Media (competitive avatars). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets and sponsored tx remove friction from competitive gameplay actions. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessAvatarBattles.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessAvatarBattles /// @notice Compete in avatar battles with gas-free wallet authentication and sponsored match transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessAvatarBattles { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessAvatarBattles` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessAvatarBattles.sol GaslessAvatarBattles` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every competitive avatars action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-gasless-avatar-gifting-10 · games · Gasless Avatar Gifting ===== Pitch: Send avatar skins and items as gifts without any gas fees. Page: https://arcprompts.lovable.app/ideas/games-gasless-avatar-gifting-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Avatar Gifting" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Send avatar skins and items as gifts without any gas fees. Discipline: Game Design & Interactive Media (social gifting). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social wallets and sponsored transactions remove friction from gifting processes. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessAvatarGifting.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessAvatarGifting /// @notice Send avatar skins and items as gifts without any gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessAvatarGifting { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessAvatarGifting` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessAvatarGifting.sol GaslessAvatarGifting` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every social gifting action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-gasless-guilds-0 · games · Gasless Guilds ===== Pitch: Seamlessly create and join guilds with gas-free onboarding and instant member transactions. Page: https://arcprompts.lovable.app/ideas/games-gasless-guilds-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Guilds" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Seamlessly create and join guilds with gas-free onboarding and instant member transactions. Discipline: Game Design & Interactive Media (multiplayer coordination). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored tx enables gasless social wallet integration, vital for frictionless group formation. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessGuilds.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessGuilds /// @notice Seamlessly create and join guilds with gas-free onboarding and instant member transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessGuilds { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessGuilds` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessGuilds.sol GaslessGuilds` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every multiplayer coordination action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-gasless-streaming-rewards-19 · games · Gasless Streaming Rewards ===== Pitch: Reward game stream viewers with gas-free wallet transactions sponsored by creators. Page: https://arcprompts.lovable.app/ideas/games-gasless-streaming-rewards-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Streaming Rewards" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Reward game stream viewers with gas-free wallet transactions sponsored by creators. Discipline: Game Design & Interactive Media (viewer incentives). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet and sponsored tx enable frictionless reward distribution to viewers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessStreamingRewards.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessStreamingRewards /// @notice Reward game stream viewers with gas-free wallet transactions sponsored by creators. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessStreamingRewards { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessStreamingRewards` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessStreamingRewards.sol GaslessStreamingRewards` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every viewer incentives action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-gasless-xr-art-swap-5 · games · Gasless XR Art Swap ===== Pitch: Trade XR art pieces effortlessly with embedded wallets and sponsored transactions. Page: https://arcprompts.lovable.app/ideas/games-gasless-xr-art-swap-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless XR Art Swap" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Trade XR art pieces effortlessly with embedded wallets and sponsored transactions. Discipline: Game Design & Interactive Media (interactive art exchange). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet with sponsored tx removes gas friction in art trades. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessXRArtSwap.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessXRArtSwap /// @notice Trade XR art pieces effortlessly with embedded wallets and sponsored transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessXRArtSwap { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessXRArtSwap` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessXRArtSwap.sol GaslessXRArtSwap` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every interactive art exchange action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-gesture-control-assets-23 · games · Gesture-Control Assets ===== Pitch: Mint unique XR gesture control assets as NFTs ensuring original creator rights. Page: https://arcprompts.lovable.app/ideas/games-gesture-control-assets-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gesture-Control Assets" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint unique XR gesture control assets as NFTs ensuring original creator rights. Discipline: Game Design & Interactive Media (XR gesture interaction). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance binds gesture assets securely to creators’ IPFS content. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GestureControlAssets.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title GestureControlAssets /// @notice ERC-721 provenance for: Mint unique XR gesture control assets as NFTs ensuring original creator rights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract GestureControlAssets is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("GestureControlAssets", "GESTUR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GestureControlAssets` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GestureControlAssets.sol GestureControlAssets` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a XR gesture interaction, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-glyph-pattern-codex-9 · games · Glyph Pattern Codex ===== Pitch: Archive unique glyphs and icon sets on IPFS for universal use in games and media. Page: https://arcprompts.lovable.app/ideas/games-glyph-pattern-codex-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Glyph Pattern Codex" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Archive unique glyphs and icon sets on IPFS for universal use in games and media. Discipline: Game Design & Interactive Media (symbol and icon design). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Permanent CID storage enables universal referencing of symbol assets without centralized control. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GlyphPatternCodex.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentGlyphPatternCodex /// @notice Archive unique glyphs and icon sets on IPFS for universal use in games and media. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentGlyphPatternCodex { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GlyphPatternCodex` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GlyphPatternCodex.sol GlyphPatternCodex` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a symbol and icon design, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-haptic-asset-chain-16 · games · Haptic Asset Chain ===== Pitch: Mint original haptic feedback patterns for XR as provenance-verified NFTs. Page: https://arcprompts.lovable.app/ideas/games-haptic-asset-chain-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Haptic Asset Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint original haptic feedback patterns for XR as provenance-verified NFTs. Discipline: Game Design & Interactive Media (tactile feedback design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain minting authenticates creator ownership of haptic interaction data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/HapticAssetChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title HapticAssetChain /// @notice ERC-721 provenance for: Mint original haptic feedback patterns for XR as provenance-verified NFTs. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract HapticAssetChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("HapticAssetChain", "HAPTIC") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs HapticAssetChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/HapticAssetChain.sol HapticAssetChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a tactile feedback design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-immersive-narrative-nodes-21 · games · Immersive Narrative Nodes ===== Pitch: Store branching narrative nodes permanently to enable expansive, decentralized interactive storytelling. Page: https://arcprompts.lovable.app/ideas/games-immersive-narrative-nodes-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Immersive Narrative Nodes" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store branching narrative nodes permanently to enable expansive, decentralized interactive storytelling. Discipline: Game Design & Interactive Media (branching story data). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS ensures permanent availability of narrative node JSONs for collaborative story ecosystems. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ImmersiveNarrativeNodes.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentImmersiveNarrativeNodes /// @notice Store branching narrative nodes permanently to enable expansive, decentralized interactive storytelling. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentImmersiveNarrativeNodes { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ImmersiveNarrativeNodes` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ImmersiveNarrativeNodes.sol ImmersiveNarrativeNodes` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a branching story data, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-immutable-scoreboards-4 · games · Immutable Scoreboards ===== Pitch: Store game scores on Sepolia to prevent cheating and create permanent leaderboards. Page: https://arcprompts.lovable.app/ideas/games-immutable-scoreboards-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Immutable Scoreboards" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store game scores on Sepolia to prevent cheating and create permanent leaderboards. Discipline: Game Design & Interactive Media (competitive scoring). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts guarantee score tamper-resistance and verifiable competition. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ImmutableScoreboards.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ImmutableScoreboards /// @notice Store game scores on Sepolia to prevent cheating and create permanent leaderboards. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ImmutableScoreboards { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ImmutableScoreboards` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ImmutableScoreboards.sol ImmutableScoreboards` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a competitive scoring action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-instant-nft-trades-9 · games · Instant NFT Trades ===== Pitch: Buy and sell game NFTs instantly with embedded wallets and zero gas fees. Page: https://arcprompts.lovable.app/ideas/games-instant-nft-trades-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Instant NFT Trades" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Buy and sell game NFTs instantly with embedded wallets and zero gas fees. Discipline: Game Design & Interactive Media (marketplace integration). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet plus sponsored tx allows seamless, gasless NFT marketplace transactions. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InstantNFTTrades.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogInstantNFTTrades /// @notice Buy and sell game NFTs instantly with embedded wallets and zero gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogInstantNFTTrades { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InstantNFTTrades` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InstantNFTTrades.sol InstantNFTTrades` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every marketplace integration action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-interactive-comic-archive-7 · games · Interactive Comic Archive ===== Pitch: Pin interactive comic panels and scripts securely to enable decentralized reader-driven narratives. Page: https://arcprompts.lovable.app/ideas/games-interactive-comic-archive-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Interactive Comic Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin interactive comic panels and scripts securely to enable decentralized reader-driven narratives. Discipline: Game Design & Interactive Media (visual storytelling). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS provides permanent decentralized hosting for branching comic content and metadata. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InteractiveComicArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentInteractiveComicArchive /// @notice Pin interactive comic panels and scripts securely to enable decentralized reader-driven narratives. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentInteractiveComicArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InteractiveComicArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InteractiveComicArchive.sol InteractiveComicArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a visual storytelling, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-interactive-comics-chain-6 · games · Interactive Comics Chain ===== Pitch: Securely mint interactive comic panels as unique NFTs with onchain provenance. Page: https://arcprompts.lovable.app/ideas/games-interactive-comics-chain-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Interactive Comics Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely mint interactive comic panels as unique NFTs with onchain provenance. Discipline: Game Design & Interactive Media (interactive storytelling). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT minting uniquely ties interactive story elements to creators’ IPFS data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InteractiveComicsChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title InteractiveComicsChain /// @notice ERC-721 provenance for: Securely mint interactive comic panels as unique NFTs with onchain provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract InteractiveComicsChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("InteractiveComicsChain", "INTERA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InteractiveComicsChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InteractiveComicsChain.sol InteractiveComicsChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a interactive storytelling, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-interactive-map-nfts-21 · games · Interactive Map NFTs ===== Pitch: Mint original interactive game maps as NFTs with verified creator provenance. Page: https://arcprompts.lovable.app/ideas/games-interactive-map-nfts-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Interactive Map NFTs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint original interactive game maps as NFTs with verified creator provenance. Discipline: Game Design & Interactive Media (game world mapping). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT minting secures unique ownership of created game maps onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InteractiveMapNFTs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title InteractiveMapNFTs /// @notice ERC-721 provenance for: Mint original interactive game maps as NFTs with verified creator provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract InteractiveMapNFTs is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("InteractiveMapNFTs", "INTERA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InteractiveMapNFTs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InteractiveMapNFTs.sol InteractiveMapNFTs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a game world mapping, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-interactive-music-nfts-13 · games · Interactive Music NFTs ===== Pitch: Mint unique interactive game soundtrack remixes with onchain provenance. Page: https://arcprompts.lovable.app/ideas/games-interactive-music-nfts-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Interactive Music NFTs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint unique interactive game soundtrack remixes with onchain provenance. Discipline: Game Design & Interactive Media (game soundtrack remixing). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs validate authentic creator rights over interactive music layers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InteractiveMusicNFTs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title InteractiveMusicNFTs /// @notice ERC-721 provenance for: Mint unique interactive game soundtrack remixes with onchain provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract InteractiveMusicNFTs is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("InteractiveMusicNFTs", "INTERA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InteractiveMusicNFTs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InteractiveMusicNFTs.sol InteractiveMusicNFTs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a game soundtrack remixing, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-interactive-soundscapes-20 · games · Interactive Soundscapes ===== Pitch: Pin rich, layered ambient soundscapes permanently for immersive and customizable game atmospheres. Page: https://arcprompts.lovable.app/ideas/games-interactive-soundscapes-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Interactive Soundscapes" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin rich, layered ambient soundscapes permanently for immersive and customizable game atmospheres. Discipline: Game Design & Interactive Media (ambient audio curation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS guarantees persistent access to complex audio compositions for game environments. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InteractiveSoundscapes.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentInteractiveSoundscapes /// @notice Pin rich, layered ambient soundscapes permanently for immersive and customizable game atmospheres. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentInteractiveSoundscapes { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InteractiveSoundscapes` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InteractiveSoundscapes.sol InteractiveSoundscapes` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a ambient audio curation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-level-chain-creator-8 · games · Level Chain Creator ===== Pitch: Mint custom game levels as NFTs with proof of originality and ownership onchain. Page: https://arcprompts.lovable.app/ideas/games-level-chain-creator-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Level Chain Creator" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint custom game levels as NFTs with proof of originality and ownership onchain. Discipline: Game Design & Interactive Media (game level design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance ensures unique ownership of user-created game levels. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LevelChainCreator.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title LevelChainCreator /// @notice ERC-721 provenance for: Mint custom game levels as NFTs with proof of originality and ownership onchain. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract LevelChainCreator is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("LevelChainCreator", "LEVELC") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LevelChainCreator` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LevelChainCreator.sol LevelChainCreator` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a game level design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-lore-chain-artifacts-11 · games · Lore Chain Artifacts ===== Pitch: Mint in-game lore entries as NFTs to guarantee original creation and provenance. Page: https://arcprompts.lovable.app/ideas/games-lore-chain-artifacts-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Lore Chain Artifacts" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint in-game lore entries as NFTs to guarantee original creation and provenance. Discipline: Game Design & Interactive Media (game lore documentation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance confirms authenticity of lore content linked on IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LoreChainArtifacts.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title LoreChainArtifacts /// @notice ERC-721 provenance for: Mint in-game lore entries as NFTs to guarantee original creation and provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract LoreChainArtifacts is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("LoreChainArtifacts", "LORECH") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LoreChainArtifacts` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LoreChainArtifacts.sol LoreChainArtifacts` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a game lore documentation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-lore-collaboration-board-12 · games · Lore Collaboration Board ===== Pitch: Enable community-driven lore contributions to be pinned permanently, ensuring collective narrative ownership. Page: https://arcprompts.lovable.app/ideas/games-lore-collaboration-board-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Lore Collaboration Board" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable community-driven lore contributions to be pinned permanently, ensuring collective narrative ownership. Discipline: Game Design & Interactive Media (community storytelling). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS immutably stores community content, preventing censorship or loss. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LoreCollaborationBoard.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentLoreCollaborationBoard /// @notice Enable community-driven lore contributions to be pinned permanently, ensuring collective narrative ownership. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentLoreCollaborationBoard { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LoreCollaborationBoard` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LoreCollaborationBoard.sol LoreCollaborationBoard` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a community storytelling, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-lore-token-index-18 · games · Lore Token Index ===== Pitch: Create permanent indexes linking lore tokens to their corresponding IPFS-hosted metadata manifests. Page: https://arcprompts.lovable.app/ideas/games-lore-token-index-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Lore Token Index" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create permanent indexes linking lore tokens to their corresponding IPFS-hosted metadata manifests. Discipline: Game Design & Interactive Media (metadata indexing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata IPFS enables verifiable, permanent linkage between game tokens and their lore data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LoreTokenIndex.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentLoreTokenIndex /// @notice Create permanent indexes linking lore tokens to their corresponding IPFS-hosted metadata manifests. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentLoreTokenIndex { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LoreTokenIndex` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LoreTokenIndex.sol LoreTokenIndex` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a metadata indexing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-mod-manifest-vault-10 · games · Mod Manifest Vault ===== Pitch: Pin and share mod manifests permanently to enable decentralized mod discovery and trust. Page: https://arcprompts.lovable.app/ideas/games-mod-manifest-vault-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Mod Manifest Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin and share mod manifests permanently to enable decentralized mod discovery and trust. Discipline: Game Design & Interactive Media (game modification metadata). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS guarantees long-term availability and integrity of mod metadata across platforms. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ModManifestVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentModManifestVault /// @notice Pin and share mod manifests permanently to enable decentralized mod discovery and trust. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentModManifestVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ModManifestVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ModManifestVault.sol ModManifestVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a game modification metadata, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-multiplayer-mod-sync-19 · games · Multiplayer Mod Sync ===== Pitch: Pin mods and game asset manifests to IPFS for real-time multiplayer mod synchronization. Page: https://arcprompts.lovable.app/ideas/games-multiplayer-mod-sync-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Multiplayer Mod Sync" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin mods and game asset manifests to IPFS for real-time multiplayer mod synchronization. Discipline: Game Design & Interactive Media (cross-user asset distribution). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS provides distributed, permanent access to synchronize assets across player nodes. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MultiplayerModSync.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMultiplayerModSync /// @notice Pin mods and game asset manifests to IPFS for real-time multiplayer mod synchronization. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMultiplayerModSync { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MultiplayerModSync` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MultiplayerModSync.sol MultiplayerModSync` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a cross-user asset distribution, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-multiplayer-skin-chain-18 · games · Multiplayer Skin Chain ===== Pitch: Mint unique multiplayer game skins as NFTs with verifiable creator provenance. Page: https://arcprompts.lovable.app/ideas/games-multiplayer-skin-chain-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Multiplayer Skin Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint unique multiplayer game skins as NFTs with verifiable creator provenance. Discipline: Game Design & Interactive Media (cosmetic item creation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance certifies cosmetic originality and ownership in multiplayer games. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MultiplayerSkinChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MultiplayerSkinChain /// @notice ERC-721 provenance for: Mint unique multiplayer game skins as NFTs with verifiable creator provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MultiplayerSkinChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MultiplayerSkinChain", "MULTIP") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MultiplayerSkinChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MultiplayerSkinChain.sol MultiplayerSkinChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a cosmetic item creation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-narrative-voice-tokens-20 · games · Narrative Voice Tokens ===== Pitch: Mint original voiceover clips for games as provenance-verified NFTs. Page: https://arcprompts.lovable.app/ideas/games-narrative-voice-tokens-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Narrative Voice Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint original voiceover clips for games as provenance-verified NFTs. Discipline: Game Design & Interactive Media (voice acting assets). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance confirms voice asset originality and creator rights. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrativeVoiceTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title NarrativeVoiceTokens /// @notice ERC-721 provenance for: Mint original voiceover clips for games as provenance-verified NFTs. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract NarrativeVoiceTokens is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("NarrativeVoiceTokens", "NARRAT") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrativeVoiceTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrativeVoiceTokens.sol NarrativeVoiceTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a voice acting assets, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-onchain-avatar-store-3 · games · Onchain Avatar Store ===== Pitch: Buy and customize avatars with zero gas fees using embedded wallets and sponsored transactions. Page: https://arcprompts.lovable.app/ideas/games-onchain-avatar-store-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Avatar Store" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Buy and customize avatars with zero gas fees using embedded wallets and sponsored transactions. Discipline: Game Design & Interactive Media (digital fashion). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet plus sponsored tx eliminates gas barriers in avatar item purchases. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainAvatarStore.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogOnchainAvatarStore /// @notice Buy and customize avatars with zero gas fees using embedded wallets and sponsored transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogOnchainAvatarStore { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainAvatarStore` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainAvatarStore.sol OnchainAvatarStore` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every digital fashion action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-onchain-game-jam-scores-17 · games · Onchain Game Jam Scores ===== Pitch: Publish and verify game jam scores on Sepolia to ensure fairness and public recognition. Page: https://arcprompts.lovable.app/ideas/games-onchain-game-jam-scores-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Game Jam Scores" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Publish and verify game jam scores on Sepolia to ensure fairness and public recognition. Discipline: Game Design & Interactive Media (competition scoring). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts provide immutable score records for competitive transparency. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainGameJamScores.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainGameJamScores /// @notice Publish and verify game jam scores on Sepolia to ensure fairness and public recognition. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainGameJamScores { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainGameJamScores` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainGameJamScores.sol OnchainGameJamScores` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a competition scoring action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-onchain-interactive-artpieces-24 · games · Onchain Interactive Artpieces ===== Pitch: Create interactive artworks whose states and ownership are controlled via Sepolia smart contracts. Page: https://arcprompts.lovable.app/ideas/games-onchain-interactive-artpieces-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Interactive Artpieces" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create interactive artworks whose states and ownership are controlled via Sepolia smart contracts. Discipline: Game Design & Interactive Media (generative art). Onchain primitive: Arc smart contract. Why this primitive: Sepolia supports programmable art with provable provenance and interaction logs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainInteractiveArtpieces.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainInteractiveArtpieces /// @notice Create interactive artworks whose states and ownership are controlled via Sepolia smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainInteractiveArtpieces { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainInteractiveArtpieces` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainInteractiveArtpieces.sol OnchainInteractiveArtpieces` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a generative art action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-onchain-multiplayer-matchmaking-9 · games · Onchain Multiplayer Matchmaking ===== Pitch: Use Sepolia to transparently pair players based on verified criteria ensuring fairness. Page: https://arcprompts.lovable.app/ideas/games-onchain-multiplayer-matchmaking-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Multiplayer Matchmaking" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Use Sepolia to transparently pair players based on verified criteria ensuring fairness. Discipline: Game Design & Interactive Media (match coordination). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts enable decentralized, tamper-proof matchmaking logic. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainMultiplayerMatchmaking.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainMultiplayerMatchmaking /// @notice Use Sepolia to transparently pair players based on verified criteria ensuring fairness. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainMultiplayerMatchmaking { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainMultiplayerMatchmaking` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainMultiplayerMatchmaking.sol OnchainMultiplayerMatchmaking` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a match coordination action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-onchain-narrative-branches-6 · games · Onchain Narrative Branches ===== Pitch: Record player story choices on Sepolia to create transparent, shared branching narratives. Page: https://arcprompts.lovable.app/ideas/games-onchain-narrative-branches-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Narrative Branches" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record player story choices on Sepolia to create transparent, shared branching narratives. Discipline: Game Design & Interactive Media (storytelling). Onchain primitive: Arc smart contract. Why this primitive: Sepolia offers immutable audit trails for branching decisions enhancing shared story experiences. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainNarrativeBranches.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainNarrativeBranches /// @notice Record player story choices on Sepolia to create transparent, shared branching narratives. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainNarrativeBranches { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainNarrativeBranches` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainNarrativeBranches.sol OnchainNarrativeBranches` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a storytelling action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-onchain-puzzle-locks-3 · games · Onchain Puzzle Locks ===== Pitch: Design puzzles whose solutions must be proven onchain via Sepolia smart contracts to unlock game content. Page: https://arcprompts.lovable.app/ideas/games-onchain-puzzle-locks-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Puzzle Locks" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Design puzzles whose solutions must be proven onchain via Sepolia smart contracts to unlock game content. Discipline: Game Design & Interactive Media (interactive puzzles). Onchain primitive: Arc smart contract. Why this primitive: Sepolia ensures unique, verifiable puzzle solution submissions maintained transparently. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainPuzzleLocks.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainPuzzleLocks /// @notice Design puzzles whose solutions must be proven onchain via Sepolia smart contracts to unlock game content. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainPuzzleLocks { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainPuzzleLocks` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainPuzzleLocks.sol OnchainPuzzleLocks` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a interactive puzzles action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-onchain-quest-ledger-0 · games · Onchain Quest Ledger ===== Pitch: Securely log player quest progress and achievements on Ethereum Sepolia to prove in-game milestones. Page: https://arcprompts.lovable.app/ideas/games-onchain-quest-ledger-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Quest Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely log player quest progress and achievements on Ethereum Sepolia to prove in-game milestones. Discipline: Game Design & Interactive Media (quest tracking). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts enable tamper-proof, transparent quest data recording accessible to all parties. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainQuestLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainQuestLedger /// @notice Securely log player quest progress and achievements on Ethereum Sepolia to prove in-game milestones. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainQuestLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainQuestLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainQuestLedger.sol OnchainQuestLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a quest tracking action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-onchain-scavenger-hunts-13 · games · Onchain Scavenger Hunts ===== Pitch: Create geolocated scavenger hunts tracked and verified on Sepolia to reward player discoveries. Page: https://arcprompts.lovable.app/ideas/games-onchain-scavenger-hunts-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Scavenger Hunts" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create geolocated scavenger hunts tracked and verified on Sepolia to reward player discoveries. Discipline: Game Design & Interactive Media (interactive exploration). Onchain primitive: Arc smart contract. Why this primitive: Sepolia supports timestamped, location-proof recording via smart contracts. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainScavengerHunts.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainScavengerHunts /// @notice Create geolocated scavenger hunts tracked and verified on Sepolia to reward player discoveries. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainScavengerHunts { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainScavengerHunts` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainScavengerHunts.sol OnchainScavengerHunts` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a interactive exploration action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-onchain-skill-trees-15 · games · Onchain Skill Trees ===== Pitch: Record and validate player skill tree choices on Sepolia ensuring unique character builds are guaranteed. Page: https://arcprompts.lovable.app/ideas/games-onchain-skill-trees-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Skill Trees" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record and validate player skill tree choices on Sepolia ensuring unique character builds are guaranteed. Discipline: Game Design & Interactive Media (character progression). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts maintain immutable records of progression states. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainSkillTrees.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainSkillTrees /// @notice Record and validate player skill tree choices on Sepolia ensuring unique character builds are guaranteed. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainSkillTrees { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainSkillTrees` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainSkillTrees.sol OnchainSkillTrees` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a character progression action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-onchain-speedrun-verification-21 · games · Onchain Speedrun Verification ===== Pitch: Verify and timestamp speedrun attempts on Sepolia smart contracts to prevent cheating. Page: https://arcprompts.lovable.app/ideas/games-onchain-speedrun-verification-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Speedrun Verification" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Verify and timestamp speedrun attempts on Sepolia smart contracts to prevent cheating. Discipline: Game Design & Interactive Media (challenge validation). Onchain primitive: Arc smart contract. Why this primitive: Sepolia provides immutable, verifiable timestamping for performance validation. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainSpeedrunVerification.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainSpeedrunVerification /// @notice Verify and timestamp speedrun attempts on Sepolia smart contracts to prevent cheating. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainSpeedrunVerification { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainSpeedrunVerification` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainSpeedrunVerification.sol OnchainSpeedrunVerification` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a challenge validation action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-onchain-virtual-economies-11 · games · Onchain Virtual Economies ===== Pitch: Manage in-game currency and item economies via Sepolia smart contracts for secure, transparent transactions. Page: https://arcprompts.lovable.app/ideas/games-onchain-virtual-economies-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Virtual Economies" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Manage in-game currency and item economies via Sepolia smart contracts for secure, transparent transactions. Discipline: Game Design & Interactive Media (economy management). Onchain primitive: Arc smart contract. Why this primitive: Sepolia provides decentralized control and auditability of virtual economy flows. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainVirtualEconomies.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainVirtualEconomies /// @notice Manage in-game currency and item economies via Sepolia smart contracts for secure, transparent transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainVirtualEconomies { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainVirtualEconomies` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainVirtualEconomies.sol OnchainVirtualEconomies` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a economy management action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-onchain-vr-experience-logs-19 · games · Onchain VR Experience Logs ===== Pitch: Record user interactions in VR on Sepolia for secure session histories and experience sharing. Page: https://arcprompts.lovable.app/ideas/games-onchain-vr-experience-logs-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain VR Experience Logs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record user interactions in VR on Sepolia for secure session histories and experience sharing. Discipline: Game Design & Interactive Media (immersive interaction). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts ensure unalterable onchain interaction records for immersive experiences. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainVRExperienceLogs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainVRExperienceLogs /// @notice Record user interactions in VR on Sepolia for secure session histories and experience sharing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainVRExperienceLogs { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainVRExperienceLogs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainVRExperienceLogs.sol OnchainVRExperienceLogs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a immersive interaction action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-pixel-lore-vault-0 · games · Pixel Lore Vault ===== Pitch: Permanently store and share evolving game storylines and lore across player communities. Page: https://arcprompts.lovable.app/ideas/games-pixel-lore-vault-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Pixel Lore Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Permanently store and share evolving game storylines and lore across player communities. Discipline: Game Design & Interactive Media (game narrative archiving). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata IPFS ensures immutable, permanent lore archives accessible worldwide without centralized servers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PixelLoreVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPixelLoreVault /// @notice Permanently store and share evolving game storylines and lore across player communities. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPixelLoreVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PixelLoreVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PixelLoreVault.sol PixelLoreVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a game narrative archiving, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-pixel-provenance-1 · games · Pixel Provenance ===== Pitch: Mint and verify original pixel art sprites as unique NFTs for games and assets. Page: https://arcprompts.lovable.app/ideas/games-pixel-provenance-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Pixel Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint and verify original pixel art sprites as unique NFTs for games and assets. Discipline: Game Design & Interactive Media (pixel art creation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance certifies original pixel art ownership and distribution onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PixelProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PixelProvenance /// @notice ERC-721 provenance for: Mint and verify original pixel art sprites as unique NFTs for games and assets. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PixelProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PixelProvenance", "PIXELP") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PixelProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PixelProvenance.sol PixelProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a pixel art creation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-playable-prop-archive-13 · games · Playable Prop Archive ===== Pitch: Permanently store props and interactive assets for reusable, verifiable game object libraries. Page: https://arcprompts.lovable.app/ideas/games-playable-prop-archive-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Playable Prop Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Permanently store props and interactive assets for reusable, verifiable game object libraries. Discipline: Game Design & Interactive Media (asset permanence). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS via Pinata ensures assets persist beyond single game lifecycles securely. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlayablePropArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPlayablePropArchive /// @notice Permanently store props and interactive assets for reusable, verifiable game object libraries. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPlayablePropArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlayablePropArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlayablePropArchive.sol PlayablePropArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a asset permanence, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-player-created-onchain-items-18 · games · Player-Created Onchain Items ===== Pitch: Allow players to mint and trade custom items on Sepolia smart contracts within games. Page: https://arcprompts.lovable.app/ideas/games-player-created-onchain-items-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Player-Created Onchain Items" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Allow players to mint and trade custom items on Sepolia smart contracts within games. Discipline: Game Design & Interactive Media (user-generated content). Onchain primitive: Arc smart contract. Why this primitive: Sepolia's verified contracts facilitate trust in player-created asset ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlayerCreatedOnchainItems.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PlayerCreatedOnchainItems /// @notice Allow players to mint and trade custom items on Sepolia smart contracts within games. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PlayerCreatedOnchainItems { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlayerCreatedOnchainItems` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlayerCreatedOnchainItems.sol PlayerCreatedOnchainItems` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a user-generated content action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-player-reputation-ledger-2 · games · Player Reputation Ledger ===== Pitch: Create immutable player reputation scores recorded on Sepolia to foster fair matchmaking and collaboration. Page: https://arcprompts.lovable.app/ideas/games-player-reputation-ledger-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Player Reputation Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create immutable player reputation scores recorded on Sepolia to foster fair matchmaking and collaboration. Discipline: Game Design & Interactive Media (community trust). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts secure trust data on an open, censorship-resistant platform. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlayerReputationLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PlayerReputationLedger /// @notice Create immutable player reputation scores recorded on Sepolia to foster fair matchmaking and collaboration. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PlayerReputationLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlayerReputationLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlayerReputationLedger.sol PlayerReputationLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a community trust action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-privy-challenge-leaderboards-21 · games · Privy Challenge Leaderboards ===== Pitch: Track daily challenges and leaderboards with gasless wallet logins and sponsored updates. Page: https://arcprompts.lovable.app/ideas/games-privy-challenge-leaderboards-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Privy Challenge Leaderboards" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track daily challenges and leaderboards with gasless wallet logins and sponsored updates. Discipline: Game Design & Interactive Media (competitive tracking). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored tx supports real-time leaderboard updates without user gas. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PrivyChallengeLeaderboards.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPrivyChallengeLeaderboards /// @notice Track daily challenges and leaderboards with gasless wallet logins and sponsored updates. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPrivyChallengeLeaderboards { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PrivyChallengeLeaderboards` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PrivyChallengeLeaderboards.sol PrivyChallengeLeaderboards` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every competitive tracking action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-privy-crafting-market-18 · games · Privy Crafting Market ===== Pitch: Trade crafted game items with gasless wallet sign-in and sponsored trades. Page: https://arcprompts.lovable.app/ideas/games-privy-crafting-market-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Privy Crafting Market" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Trade crafted game items with gasless wallet sign-in and sponsored trades. Discipline: Game Design & Interactive Media (player economies). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social and sponsored tx improve player-to-player economy with no gas fees. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PrivyCraftingMarket.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPrivyCraftingMarket /// @notice Trade crafted game items with gasless wallet sign-in and sponsored trades. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPrivyCraftingMarket { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PrivyCraftingMarket` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PrivyCraftingMarket.sol PrivyCraftingMarket` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every player economies action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-privy-game-jam-6 · games · Privy Game Jam ===== Pitch: Host game jams with onchain identity and gas-free participation transactions. Page: https://arcprompts.lovable.app/ideas/games-privy-game-jam-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Privy Game Jam" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Host game jams with onchain identity and gas-free participation transactions. Discipline: Game Design & Interactive Media (developer community). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored tx allows easy gasless user onboarding for events. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PrivyGameJam.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPrivyGameJam /// @notice Host game jams with onchain identity and gas-free participation transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPrivyGameJam { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PrivyGameJam` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PrivyGameJam.sol PrivyGameJam` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every developer community action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-privy-interactive-ads-24 · games · Privy Interactive Ads ===== Pitch: Engage users with interactive ads that deliver gasless rewards via embedded wallets. Page: https://arcprompts.lovable.app/ideas/games-privy-interactive-ads-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Privy Interactive Ads" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Engage users with interactive ads that deliver gasless rewards via embedded wallets. Discipline: Game Design & Interactive Media (ad integration). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet plus sponsored tx allows rewarding ad engagements without gas friction. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PrivyInteractiveAds.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPrivyInteractiveAds /// @notice Engage users with interactive ads that deliver gasless rewards via embedded wallets. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPrivyInteractiveAds { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PrivyInteractiveAds` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PrivyInteractiveAds.sol PrivyInteractiveAds` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every ad integration action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-privy-puzzle-rewards-15 · games · Privy Puzzle Rewards ===== Pitch: Earn puzzle completion rewards with gas-free wallet sign-in and transaction sponsorship. Page: https://arcprompts.lovable.app/ideas/games-privy-puzzle-rewards-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Privy Puzzle Rewards" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Earn puzzle completion rewards with gas-free wallet sign-in and transaction sponsorship. Discipline: Game Design & Interactive Media (casual game incentives). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored tx enables seamless reward claim without gas hassles. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PrivyPuzzleRewards.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPrivyPuzzleRewards /// @notice Earn puzzle completion rewards with gas-free wallet sign-in and transaction sponsorship. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPrivyPuzzleRewards { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PrivyPuzzleRewards` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PrivyPuzzleRewards.sol PrivyPuzzleRewards` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every casual game incentives action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-privy-scoreboard-8 · games · Privy Scoreboard ===== Pitch: Display and update player rankings with gas-free wallet sign-in and transaction updates. Page: https://arcprompts.lovable.app/ideas/games-privy-scoreboard-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Privy Scoreboard" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Display and update player rankings with gas-free wallet sign-in and transaction updates. Discipline: Game Design & Interactive Media (competitive ranking). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social wallet plus sponsored tx keeps leaderboards updated without gas costs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PrivyScoreboard.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPrivyScoreboard /// @notice Display and update player rankings with gas-free wallet sign-in and transaction updates. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPrivyScoreboard { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PrivyScoreboard` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PrivyScoreboard.sol PrivyScoreboard` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every competitive ranking action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-privy-story-worlds-12 · games · Privy Story Worlds ===== Pitch: Create and share story-driven worlds with gasless wallet logins and sponsored content publishing. Page: https://arcprompts.lovable.app/ideas/games-privy-story-worlds-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Privy Story Worlds" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create and share story-driven worlds with gasless wallet logins and sponsored content publishing. Discipline: Game Design & Interactive Media (user-generated content). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social and sponsored tx simplify content creation without gas costs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PrivyStoryWorlds.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPrivyStoryWorlds /// @notice Create and share story-driven worlds with gasless wallet logins and sponsored content publishing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPrivyStoryWorlds { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PrivyStoryWorlds` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PrivyStoryWorlds.sol PrivyStoryWorlds` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every user-generated content action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-privy-vr-lobby-2 · games · Privy VR Lobby ===== Pitch: Enter virtual lobbies with gas-free wallet login and seamless social interactions. Page: https://arcprompts.lovable.app/ideas/games-privy-vr-lobby-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Privy VR Lobby" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enter virtual lobbies with gas-free wallet login and seamless social interactions. Discipline: Game Design & Interactive Media (XR social spaces). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet with Google sign-in and sponsored tx ensures easy, gasless VR social access. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PrivyVRLobby.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPrivyVRLobby /// @notice Enter virtual lobbies with gas-free wallet login and seamless social interactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPrivyVRLobby { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PrivyVRLobby` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PrivyVRLobby.sol PrivyVRLobby` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every XR social spaces action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-procedural-map-depository-4 · games · Procedural Map Depository ===== Pitch: Securely archive and share procedurally generated game maps for community use and collaboration. Page: https://arcprompts.lovable.app/ideas/games-procedural-map-depository-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Procedural Map Depository" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely archive and share procedurally generated game maps for community use and collaboration. Discipline: Game Design & Interactive Media (map generation and sharing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata IPFS enables permanent decentralized access to dynamic map datasets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ProceduralMapDepository.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentProceduralMapDepository /// @notice Securely archive and share procedurally generated game maps for community use and collaboration. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentProceduralMapDepository { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ProceduralMapDepository` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ProceduralMapDepository.sol ProceduralMapDepository` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a map generation and sharing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-procedural-provenance-5 · games · Procedural Provenance ===== Pitch: Mint procedurally generated game art as NFTs linked to creator algorithms onchain. Page: https://arcprompts.lovable.app/ideas/games-procedural-provenance-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Procedural Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint procedurally generated game art as NFTs linked to creator algorithms onchain. Discipline: Game Design & Interactive Media (algorithmic game art). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Provenance minting ensures verifiable origin of algorithmically created assets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ProceduralProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ProceduralProvenance /// @notice ERC-721 provenance for: Mint procedurally generated game art as NFTs linked to creator algorithms onchain. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ProceduralProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ProceduralProvenance", "PROCED") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ProceduralProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ProceduralProvenance.sol ProceduralProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a algorithmic game art, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-proof-of-play-events-8 · games · Proof-of-Play Events ===== Pitch: Authenticate player participation in timed events on Sepolia for exclusive rewards and recognition. Page: https://arcprompts.lovable.app/ideas/games-proof-of-play-events-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Proof-of-Play Events" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate player participation in timed events on Sepolia for exclusive rewards and recognition. Discipline: Game Design & Interactive Media (event verification). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts provide immutable proof of player interaction and eligibility. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ProofofPlayEvents.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ProofofPlayEvents /// @notice Authenticate player participation in timed events on Sepolia for exclusive rewards and recognition. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ProofofPlayEvents { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ProofofPlayEvents` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ProofofPlayEvents.sol ProofofPlayEvents` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a event verification action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-provenance-playbooks-0 · games · Provenance Playbooks ===== Pitch: Create branching story games with verifiable original story assets minted onchain. Page: https://arcprompts.lovable.app/ideas/games-provenance-playbooks-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Provenance Playbooks" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create branching story games with verifiable original story assets minted onchain. Discipline: Game Design & Interactive Media (game narrative design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Ensures unique authorship and provenance of game narrative elements through NFT minting. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ProvenancePlaybooks.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ProvenancePlaybooks /// @notice ERC-721 provenance for: Create branching story games with verifiable original story assets minted onchain. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ProvenancePlaybooks is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ProvenancePlaybooks", "PROVEN") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ProvenancePlaybooks` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ProvenancePlaybooks.sol ProvenancePlaybooks` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a game narrative design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-puzzle-chain-creator-19 · games · Puzzle Chain Creator ===== Pitch: Mint original interactive puzzles as NFTs proving unique creator ownership. Page: https://arcprompts.lovable.app/ideas/games-puzzle-chain-creator-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Puzzle Chain Creator" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint original interactive puzzles as NFTs proving unique creator ownership. Discipline: Game Design & Interactive Media (interactive puzzle design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Provenance minting records puzzle originality and creator IPFS data onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PuzzleChainCreator.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PuzzleChainCreator /// @notice ERC-721 provenance for: Mint original interactive puzzles as NFTs proving unique creator ownership. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PuzzleChainCreator is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PuzzleChainCreator", "PUZZLE") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PuzzleChainCreator` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PuzzleChainCreator.sol PuzzleChainCreator` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a interactive puzzle design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-puzzle-logic-cache-17 · games · Puzzle Logic Cache ===== Pitch: Store complex puzzle logic and solution paths immutably for community puzzle sharing and remixing. Page: https://arcprompts.lovable.app/ideas/games-puzzle-logic-cache-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Puzzle Logic Cache" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store complex puzzle logic and solution paths immutably for community puzzle sharing and remixing. Discipline: Game Design & Interactive Media (game mechanic storage). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Immutable IPFS storage prevents tampering with puzzle mechanics, ensuring fair play. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PuzzleLogicCache.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPuzzleLogicCache /// @notice Store complex puzzle logic and solution paths immutably for community puzzle sharing and remixing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPuzzleLogicCache { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PuzzleLogicCache` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PuzzleLogicCache.sol PuzzleLogicCache` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a game mechanic storage, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-quest-chain-archive-2 · games · Quest Chain Archive ===== Pitch: Distribute permanent quest data and decision trees for replayable and modifiable game adventures. Page: https://arcprompts.lovable.app/ideas/games-quest-chain-archive-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Quest Chain Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Distribute permanent quest data and decision trees for replayable and modifiable game adventures. Discipline: Game Design & Interactive Media (interactive quest design). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS via Pinata preserves quest data immutably, enabling decentralized quest sharing. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/QuestChainArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentQuestChainArchive /// @notice Distribute permanent quest data and decision trees for replayable and modifiable game adventures. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentQuestChainArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs QuestChainArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/QuestChainArchive.sol QuestChainArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a interactive quest design, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-rare-achievement-tokens-10 · games · Rare Achievement Tokens ===== Pitch: Mint unique achievement tokens on Sepolia when players hit milestones to prove in-game skills. Page: https://arcprompts.lovable.app/ideas/games-rare-achievement-tokens-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Rare Achievement Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint unique achievement tokens on Sepolia when players hit milestones to prove in-game skills. Discipline: Game Design & Interactive Media (achievement systems). Onchain primitive: Arc smart contract. Why this primitive: Sepolia supports unique, traceable achievement token issuance tied to player actions. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/RareAchievementTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title RareAchievementTokens /// @notice Mint unique achievement tokens on Sepolia when players hit milestones to prove in-game skills. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract RareAchievementTokens { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs RareAchievementTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/RareAchievementTokens.sol RareAchievementTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a achievement systems action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-replay-provenance-9 · games · Replay Provenance ===== Pitch: Mint verified game replays as NFTs to prove originality and preserve gameplay moments. Page: https://arcprompts.lovable.app/ideas/games-replay-provenance-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Replay Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint verified game replays as NFTs to prove originality and preserve gameplay moments. Discipline: Game Design & Interactive Media (game replay sharing). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Provenance minting links replays securely to original gameplay data onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ReplayProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ReplayProvenance /// @notice ERC-721 provenance for: Mint verified game replays as NFTs to prove originality and preserve gameplay moments. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ReplayProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ReplayProvenance", "REPLAY") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ReplayProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ReplayProvenance.sol ReplayProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a game replay sharing, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-social-quest-chains-4 · games · Social Quest Chains ===== Pitch: Collaborate on story-driven quests with gasless wallet sign-in and transaction sponsorship. Page: https://arcprompts.lovable.app/ideas/games-social-quest-chains-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Social Quest Chains" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Collaborate on story-driven quests with gasless wallet sign-in and transaction sponsorship. Discipline: Game Design & Interactive Media (interactive storytelling). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored tx enables seamless collaboration with no gas hurdles. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SocialQuestChains.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSocialQuestChains /// @notice Collaborate on story-driven quests with gasless wallet sign-in and transaction sponsorship. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSocialQuestChains { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SocialQuestChains` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SocialQuestChains.sol SocialQuestChains` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every interactive storytelling action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-sound-slice-chain-2 · games · Sound Slice Chain ===== Pitch: Securely mint and track original sound samples for interactive game soundtracks. Page: https://arcprompts.lovable.app/ideas/games-sound-slice-chain-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sound Slice Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely mint and track original sound samples for interactive game soundtracks. Discipline: Game Design & Interactive Media (game audio sampling). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Provenance minting authenticates sound assets and prevents unauthorized reuse. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SoundSliceChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SoundSliceChain /// @notice ERC-721 provenance for: Securely mint and track original sound samples for interactive game soundtracks. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SoundSliceChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SoundSliceChain", "SOUNDS") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SoundSliceChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SoundSliceChain.sol SoundSliceChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a game audio sampling, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-speedrun-data-ledger-14 · games · Speedrun Data Ledger ===== Pitch: Pin detailed speedrun metadata and replay JSONs permanently for competitive integrity and sharing. Page: https://arcprompts.lovable.app/ideas/games-speedrun-data-ledger-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Speedrun Data Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin detailed speedrun metadata and replay JSONs permanently for competitive integrity and sharing. Discipline: Game Design & Interactive Media (game performance recording). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS guarantees immutable speedrun proof and replay data for transparency. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SpeedrunDataLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentSpeedrunDataLedger /// @notice Pin detailed speedrun metadata and replay JSONs permanently for competitive integrity and sharing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentSpeedrunDataLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SpeedrunDataLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SpeedrunDataLedger.sol SpeedrunDataLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a game performance recording, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-sponsored-beta-access-7 · games · Sponsored Beta Access ===== Pitch: Grant beta testing access through gasless wallet authentication and sponsored invites. Page: https://arcprompts.lovable.app/ideas/games-sponsored-beta-access-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sponsored Beta Access" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Grant beta testing access through gasless wallet authentication and sponsored invites. Discipline: Game Design & Interactive Media (user testing). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet and sponsored tx enable friction-free beta tester onboarding. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SponsoredBetaAccess.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSponsoredBetaAccess /// @notice Grant beta testing access through gasless wallet authentication and sponsored invites. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSponsoredBetaAccess { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SponsoredBetaAccess` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SponsoredBetaAccess.sol SponsoredBetaAccess` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every user testing action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-sponsored-fan-tokens-20 · games · Sponsored Fan Tokens ===== Pitch: Issue fan engagement tokens to gamers using gasless wallet onboarding and sponsored mints. Page: https://arcprompts.lovable.app/ideas/games-sponsored-fan-tokens-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sponsored Fan Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Issue fan engagement tokens to gamers using gasless wallet onboarding and sponsored mints. Discipline: Game Design & Interactive Media (community engagement). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social and sponsored tx make fan token distribution smooth and gasless. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SponsoredFanTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSponsoredFanTokens /// @notice Issue fan engagement tokens to gamers using gasless wallet onboarding and sponsored mints. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSponsoredFanTokens { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SponsoredFanTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SponsoredFanTokens.sol SponsoredFanTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every community engagement action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-sponsored-loot-drops-1 · games · Sponsored Loot Drops ===== Pitch: Distribute in-game rewards directly to players' wallets without any gas fees. Page: https://arcprompts.lovable.app/ideas/games-sponsored-loot-drops-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sponsored Loot Drops" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Distribute in-game rewards directly to players' wallets without any gas fees. Discipline: Game Design & Interactive Media (reward distribution). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored transactions allow frictionless token drops without user gas payments, improving user experience. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SponsoredLootDrops.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSponsoredLootDrops /// @notice Distribute in-game rewards directly to players' wallets without any gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSponsoredLootDrops { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SponsoredLootDrops` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SponsoredLootDrops.sol SponsoredLootDrops` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every reward distribution action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-sponsored-multiplayer-drops-23 · games · Sponsored Multiplayer Drops ===== Pitch: Distribute gasless event drops during multiplayer games with embedded wallets and sponsored tx. Page: https://arcprompts.lovable.app/ideas/games-sponsored-multiplayer-drops-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sponsored Multiplayer Drops" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Distribute gasless event drops during multiplayer games with embedded wallets and sponsored tx. Discipline: Game Design & Interactive Media (event rewards). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social and sponsored transactions ensure smooth, gas-free event drop delivery. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SponsoredMultiplayerDrops.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSponsoredMultiplayerDrops /// @notice Distribute gasless event drops during multiplayer games with embedded wallets and sponsored tx. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSponsoredMultiplayerDrops { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SponsoredMultiplayerDrops` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SponsoredMultiplayerDrops.sol SponsoredMultiplayerDrops` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every event rewards action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-sponsored-skill-boosts-11 · games · Sponsored Skill Boosts ===== Pitch: Grant skill boosts and power-ups via gasless wallet transactions. Page: https://arcprompts.lovable.app/ideas/games-sponsored-skill-boosts-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sponsored Skill Boosts" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Grant skill boosts and power-ups via gasless wallet transactions. Discipline: Game Design & Interactive Media (in-game perks). Onchain primitive: Privy social wallet on Arc. Why this primitive: Embedded wallet plus sponsored tx enables instant gas-free perk delivery. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SponsoredSkillBoosts.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSponsoredSkillBoosts /// @notice Grant skill boosts and power-ups via gasless wallet transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSponsoredSkillBoosts { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SponsoredSkillBoosts` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SponsoredSkillBoosts.sol SponsoredSkillBoosts` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every in-game perks action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-sponsored-xr-avatars-14 · games · Sponsored XR Avatars ===== Pitch: Equip XR avatars with sponsored token transactions and easy wallet sign-in. Page: https://arcprompts.lovable.app/ideas/games-sponsored-xr-avatars-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sponsored XR Avatars" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Equip XR avatars with sponsored token transactions and easy wallet sign-in. Discipline: Game Design & Interactive Media (virtual identity). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet and sponsored tx streamline avatar customization gaslessly. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SponsoredXRAvatars.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSponsoredXRAvatars /// @notice Equip XR avatars with sponsored token transactions and easy wallet sign-in. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSponsoredXRAvatars { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SponsoredXRAvatars` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SponsoredXRAvatars.sol SponsoredXRAvatars` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every virtual identity action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-sponsored-xr-exhibits-17 · games · Sponsored XR Exhibits ===== Pitch: Host XR galleries that visitors explore with gasless wallet login and sponsored interaction tx. Page: https://arcprompts.lovable.app/ideas/games-sponsored-xr-exhibits-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sponsored XR Exhibits" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Host XR galleries that visitors explore with gasless wallet login and sponsored interaction tx. Discipline: Game Design & Interactive Media (virtual galleries). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet plus sponsored tx simplifies user access to XR galleries. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SponsoredXRExhibits.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSponsoredXRExhibits /// @notice Host XR galleries that visitors explore with gasless wallet login and sponsored interaction tx. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSponsoredXRExhibits { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SponsoredXRExhibits` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SponsoredXRExhibits.sol SponsoredXRExhibits` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every virtual galleries action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-token-gated-game-access-23 · games · Token-Gated Game Access ===== Pitch: Use Sepolia smart contracts to restrict game content access via ownership of specific tokens. Page: https://arcprompts.lovable.app/ideas/games-token-gated-game-access-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Token-Gated Game Access" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Use Sepolia smart contracts to restrict game content access via ownership of specific tokens. Discipline: Game Design & Interactive Media (access control). Onchain primitive: Arc smart contract. Why this primitive: Sepolia enforces secure token-based permissions directly onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TokenGatedGameAccess.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title TokenGatedGameAccess /// @notice Use Sepolia smart contracts to restrict game content access via ownership of specific tokens. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TokenGatedGameAccess { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TokenGatedGameAccess` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TokenGatedGameAccess.sol TokenGatedGameAccess` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a access control action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-tokenized-game-guides-14 · games · Tokenized Game Guides ===== Pitch: Mint original game strategy guides as NFTs to prove and reward authorship. Page: https://arcprompts.lovable.app/ideas/games-tokenized-game-guides-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Tokenized Game Guides" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint original game strategy guides as NFTs to prove and reward authorship. Discipline: Game Design & Interactive Media (strategy content). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Provenance minting secures guide originality and creator royalties onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TokenizedGameGuides.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title TokenizedGameGuides /// @notice ERC-721 provenance for: Mint original game strategy guides as NFTs to prove and reward authorship. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TokenizedGameGuides is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("TokenizedGameGuides", "TOKENI") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TokenizedGameGuides` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TokenizedGameGuides.sol TokenizedGameGuides` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a strategy content, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-tokenized-loot-drops-1 · games · Tokenized Loot Drops ===== Pitch: Distribute rare, verifiable loot as tokens directly through Sepolia smart contracts to guarantee authenticity. Page: https://arcprompts.lovable.app/ideas/games-tokenized-loot-drops-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Tokenized Loot Drops" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Distribute rare, verifiable loot as tokens directly through Sepolia smart contracts to guarantee authenticity. Discipline: Game Design & Interactive Media (item distribution). Onchain primitive: Arc smart contract. Why this primitive: Sepolia allows automated, transparent item drops triggered by onchain game events. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TokenizedLootDrops.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title TokenizedLootDrops /// @notice Distribute rare, verifiable loot as tokens directly through Sepolia smart contracts to guarantee authenticity. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TokenizedLootDrops { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TokenizedLootDrops` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TokenizedLootDrops.sol TokenizedLootDrops` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a item distribution action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-tokenized-lore-boards-24 · games · Tokenized Lore Boards ===== Pitch: Create permanent, token-linked lore boards for collaborative and credentialed story creation. Page: https://arcprompts.lovable.app/ideas/games-tokenized-lore-boards-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Tokenized Lore Boards" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create permanent, token-linked lore boards for collaborative and credentialed story creation. Discipline: Game Design & Interactive Media (community content curation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata IPFS links lore content immutably to tokens, ensuring provenance and community edits. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TokenizedLoreBoards.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentTokenizedLoreBoards /// @notice Create permanent, token-linked lore boards for collaborative and credentialed story creation. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentTokenizedLoreBoards { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TokenizedLoreBoards` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TokenizedLoreBoards.sol TokenizedLoreBoards` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a community content curation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-transparent-rng-mechanics-14 · games · Transparent RNG Mechanics ===== Pitch: Implement verifiable random number generation on Sepolia to ensure fair game outcomes. Page: https://arcprompts.lovable.app/ideas/games-transparent-rng-mechanics-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Transparent RNG Mechanics" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Implement verifiable random number generation on Sepolia to ensure fair game outcomes. Discipline: Game Design & Interactive Media (game fairness). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts provide unbiased, publicly verifiable randomness sources. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TransparentRNGMechanics.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title TransparentRNGMechanics /// @notice Implement verifiable random number generation on Sepolia to ensure fair game outcomes. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TransparentRNGMechanics { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TransparentRNGMechanics` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TransparentRNGMechanics.sol TransparentRNGMechanics` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a game fairness action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-virtual-stage-archives-15 · games · Virtual Stage Archives ===== Pitch: Store and preserve virtual stage designs and performance manifests for XR and live events. Page: https://arcprompts.lovable.app/ideas/games-virtual-stage-archives-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Virtual Stage Archives" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store and preserve virtual stage designs and performance manifests for XR and live events. Discipline: Game Design & Interactive Media (interactive performance spaces). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata IPFS provides persistent, decentralized access to ephemeral virtual event data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VirtualStageArchives.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentVirtualStageArchives /// @notice Store and preserve virtual stage designs and performance manifests for XR and live events. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentVirtualStageArchives { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VirtualStageArchives` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VirtualStageArchives.sol VirtualStageArchives` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a interactive performance spaces, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-vr-provenance-hub-3 · games · VR Provenance Hub ===== Pitch: Mint immersive VR environments as NFTs to ensure creator authenticity and ownership. Page: https://arcprompts.lovable.app/ideas/games-vr-provenance-hub-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VR Provenance Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint immersive VR environments as NFTs to ensure creator authenticity and ownership. Discipline: Game Design & Interactive Media (XR environment design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain provenance links environments uniquely to creator IPFS-hosted data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VRProvenanceHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title VRProvenanceHub /// @notice ERC-721 provenance for: Mint immersive VR environments as NFTs to ensure creator authenticity and ownership. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VRProvenanceHub is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("VRProvenanceHub", "VRPROV") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VRProvenanceHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VRProvenanceHub.sol VRProvenanceHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a XR environment design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-xr-artifact-library-22 · games · XR Artifact Library ===== Pitch: Archive and share 3D XR artifacts via IPFS, ensuring permanent access to virtual museum pieces. Page: https://arcprompts.lovable.app/ideas/games-xr-artifact-library-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "XR Artifact Library" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Archive and share 3D XR artifacts via IPFS, ensuring permanent access to virtual museum pieces. Discipline: Game Design & Interactive Media (3D object preservation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinning 3D assets to IPFS offers decentralized, long-term storage beyond platform lifespans. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/XRArtifactLibrary.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentXRArtifactLibrary /// @notice Archive and share 3D XR artifacts via IPFS, ensuring permanent access to virtual museum pieces. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentXRArtifactLibrary { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs XRArtifactLibrary` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/XRArtifactLibrary.sol XRArtifactLibrary` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a 3D object preservation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-xr-gesture-tokens-10 · games · XR Gesture Tokens ===== Pitch: Mint original XR gesture captures as NFTs proving creator authorship. Page: https://arcprompts.lovable.app/ideas/games-xr-gesture-tokens-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "XR Gesture Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint original XR gesture captures as NFTs proving creator authorship. Discipline: Game Design & Interactive Media (motion capture NFTs). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain minting certifies unique ownership of motion capture data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/XRGestureTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title XRGestureTokens /// @notice ERC-721 provenance for: Mint original XR gesture captures as NFTs proving creator authorship. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract XRGestureTokens is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("XRGestureTokens", "XRGEST") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs XRGestureTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/XRGestureTokens.sol XRGestureTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a motion capture NFTs, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA games-xr-scene-snapshot-3 · games · XR Scene Snapshot ===== Pitch: Pin immersive XR scenes and metadata permanently for cross-platform sharing and reuse. Page: https://arcprompts.lovable.app/ideas/games-xr-scene-snapshot-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "XR Scene Snapshot" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin immersive XR scenes and metadata permanently for cross-platform sharing and reuse. Discipline: Game Design & Interactive Media (extended reality content storage). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS guarantees persistent XR assets unbound by platform constraints or server downtime. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/XRSceneSnapshot.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentXRSceneSnapshot /// @notice Pin immersive XR scenes and metadata permanently for cross-platform sharing and reuse. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentXRSceneSnapshot { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs XRSceneSnapshot` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/XRSceneSnapshot.sol XRSceneSnapshot` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a extended reality content storage, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-ai-audio-license-6 · music · AI Audio License ===== Pitch: Onchain licenses for AI-created audio to prevent misuse and ensure creator credit. Page: https://arcprompts.lovable.app/ideas/music-ai-audio-license-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AI Audio License" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Onchain licenses for AI-created audio to prevent misuse and ensure creator credit. Discipline: Music & Sound Design (AI-generated music). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts track AI output provenance and rights enforcement. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AIAudioLicense.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AIAudioLicense /// @notice Onchain licenses for AI-created audio to prevent misuse and ensure creator credit. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AIAudioLicense { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AIAudioLicense` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AIAudioLicense.sol AIAudioLicense` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a AI-generated music action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-album-art-audio-tokens-14 · music · Album Art Audio Tokens ===== Pitch: Combine album art and music provenance by minting unified NFT tokens. Page: https://arcprompts.lovable.app/ideas/music-album-art-audio-tokens-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Album Art Audio Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Combine album art and music provenance by minting unified NFT tokens. Discipline: Music & Sound Design (integrated art & sound). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Single provenance tokens immutable link both audio and visual IPFS assets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AlbumArtAudioTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title AlbumArtAudioTokens /// @notice ERC-721 provenance for: Combine album art and music provenance by minting unified NFT tokens. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AlbumArtAudioTokens is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("AlbumArtAudioTokens", "ALBUMA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AlbumArtAudioTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AlbumArtAudioTokens.sol AlbumArtAudioTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a integrated art & sound, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-ambient-track-tokens-23 · music · Ambient Track Tokens ===== Pitch: Mint NFTs for ambient compositions verifying original soundscapes. Page: https://arcprompts.lovable.app/ideas/music-ambient-track-tokens-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Ambient Track Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs for ambient compositions verifying original soundscapes. Discipline: Music & Sound Design (ambient music archives). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens preserve ambient music provenance through IPFS storage. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AmbientTrackTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title AmbientTrackTokens /// @notice ERC-721 provenance for: Mint NFTs for ambient compositions verifying original soundscapes. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AmbientTrackTokens is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("AmbientTrackTokens", "AMBIEN") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AmbientTrackTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AmbientTrackTokens.sol AmbientTrackTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a ambient music archives, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-audio-blueprint-20 · music · Audio Blueprint ===== Pitch: Archive reusable sound design templates with verified authorship and version tracking. Page: https://arcprompts.lovable.app/ideas/music-audio-blueprint-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Audio Blueprint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Archive reusable sound design templates with verified authorship and version tracking. Discipline: Music & Sound Design (sound design templates). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS hosts template files ensuring permanent access and identity proof. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AudioBlueprint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAudioBlueprint /// @notice Archive reusable sound design templates with verified authorship and version tracking. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAudioBlueprint { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AudioBlueprint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AudioBlueprint.sol AudioBlueprint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a sound design templates, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-authentic-sample-provenance-7 · music · Authentic Sample Provenance ===== Pitch: Verify sample origins onchain with free, user-friendly wallet onboarding to prevent piracy. Page: https://arcprompts.lovable.app/ideas/music-authentic-sample-provenance-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Authentic Sample Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Verify sample origins onchain with free, user-friendly wallet onboarding to prevent piracy. Discipline: Music & Sound Design (sample authentication). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets and sponsored transactions reduce friction in authenticating sample ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AuthenticSampleProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogAuthenticSampleProvenance /// @notice Verify sample origins onchain with free, user-friendly wallet onboarding to prevent piracy. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogAuthenticSampleProvenance { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AuthenticSampleProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AuthenticSampleProvenance.sol AuthenticSampleProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every sample authentication action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-beatledger-9 · music · BeatLedger ===== Pitch: Record and prove ownership of beats and instrumentals on a decentralized archive. Page: https://arcprompts.lovable.app/ideas/music-beatledger-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "BeatLedger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record and prove ownership of beats and instrumentals on a decentralized archive. Discipline: Music & Sound Design (beat ownership). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS ensures beat files and ownership proofs are immutable and always-accessible. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/BeatLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentBeatLedger /// @notice Record and prove ownership of beats and instrumentals on a decentralized archive. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentBeatLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs BeatLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/BeatLedger.sol BeatLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a beat ownership, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-blockchain-soundscapes-16 · music · Blockchain Soundscapes ===== Pitch: Share and license unique recorded soundscapes with verified origin and ownership on blockchain. Page: https://arcprompts.lovable.app/ideas/music-blockchain-soundscapes-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Blockchain Soundscapes" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Share and license unique recorded soundscapes with verified origin and ownership on blockchain. Discipline: Music & Sound Design (environmental audio). Onchain primitive: Arc smart contract. Why this primitive: Immutable records authenticate soundscape provenance and creator rights. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/BlockchainSoundscapes.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title BlockchainSoundscapes /// @notice Share and license unique recorded soundscapes with verified origin and ownership on blockchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract BlockchainSoundscapes { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs BlockchainSoundscapes` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/BlockchainSoundscapes.sol BlockchainSoundscapes` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a environmental audio action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-collaborative-audio-nfts-23 · music · Collaborative Audio NFTs ===== Pitch: Mint multi-creator audio NFTs with transparent ownership and gasless transactions. Page: https://arcprompts.lovable.app/ideas/music-collaborative-audio-nfts-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Collaborative Audio NFTs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint multi-creator audio NFTs with transparent ownership and gasless transactions. Discipline: Music & Sound Design (co-authored NFTs). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social wallets and sponsored tx ease multi-user minting processes without costs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CollaborativeAudioNFTs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogCollaborativeAudioNFTs /// @notice Mint multi-creator audio NFTs with transparent ownership and gasless transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogCollaborativeAudioNFTs { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CollaborativeAudioNFTs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CollaborativeAudioNFTs.sol CollaborativeAudioNFTs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every co-authored NFTs action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-collaborative-beat-ledger-2 · music · Collaborative Beat Ledger ===== Pitch: Record and timestamp collaborative beat creation sessions with immutable proof of contribution. Page: https://arcprompts.lovable.app/ideas/music-collaborative-beat-ledger-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Collaborative Beat Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record and timestamp collaborative beat creation sessions with immutable proof of contribution. Discipline: Music & Sound Design (beat collaboration). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts ensure transparent, tamper-proof proof of co-production. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CollaborativeBeatLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title CollaborativeBeatLedger /// @notice Record and timestamp collaborative beat creation sessions with immutable proof of contribution. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CollaborativeBeatLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CollaborativeBeatLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CollaborativeBeatLedger.sol CollaborativeBeatLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a beat collaboration action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-collaborative-loop-vault-10 · music · Collaborative Loop Vault ===== Pitch: Securely share and build loop collections with zero gas cost and social wallet access. Page: https://arcprompts.lovable.app/ideas/music-collaborative-loop-vault-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Collaborative Loop Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely share and build loop collections with zero gas cost and social wallet access. Discipline: Music & Sound Design (loop sharing). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets plus sponsored tx enable multiple collaborators to add loops frictionlessly. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CollaborativeLoopVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogCollaborativeLoopVault /// @notice Securely share and build loop collections with zero gas cost and social wallet access. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogCollaborativeLoopVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CollaborativeLoopVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CollaborativeLoopVault.sol CollaborativeLoopVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every loop sharing action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-collaborative-track-tokens-8 · music · Collaborative Track Tokens ===== Pitch: Create NFTs representing joint music projects, tracking all contributors’ rights. Page: https://arcprompts.lovable.app/ideas/music-collaborative-track-tokens-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Collaborative Track Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create NFTs representing joint music projects, tracking all contributors’ rights. Discipline: Music & Sound Design (co-creation tracking). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain tokens prove shared ownership and IPFS links store collaborative files. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CollaborativeTrackTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title CollaborativeTrackTokens /// @notice ERC-721 provenance for: Create NFTs representing joint music projects, tracking all contributors’ rights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CollaborativeTrackTokens is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("CollaborativeTrackTokens", "COLLAB") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CollaborativeTrackTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CollaborativeTrackTokens.sol CollaborativeTrackTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a co-creation tracking, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-composer-collab-14 · music · Composer Collab ===== Pitch: Securely share and timestamp collaborative compositions with permanent version control. Page: https://arcprompts.lovable.app/ideas/music-composer-collab-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Composer Collab" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely share and timestamp collaborative compositions with permanent version control. Discipline: Music & Sound Design (composition sharing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS immutably pins composition files and revision manifests for transparency. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ComposerCollab.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentComposerCollab /// @notice Securely share and timestamp collaborative compositions with permanent version control. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentComposerCollab { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ComposerCollab` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ComposerCollab.sol ComposerCollab` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a composition sharing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-composer-cue-tokens-7 · music · Composer Cue Tokens ===== Pitch: Mint original music cue NFTs to prove composer ownership and license rights. Page: https://arcprompts.lovable.app/ideas/music-composer-cue-tokens-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Composer Cue Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint original music cue NFTs to prove composer ownership and license rights. Discipline: Music & Sound Design (media scoring segments). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 provenance tokens secure music cues for synchronization licensing. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ComposerCueTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ComposerCueTokens /// @notice ERC-721 provenance for: Mint original music cue NFTs to prove composer ownership and license rights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ComposerCueTokens is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ComposerCueTokens", "COMPOS") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ComposerCueTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ComposerCueTokens.sol ComposerCueTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a media scoring segments, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-curated-sound-trails-6 · music · Curated Sound Trails ===== Pitch: Create and share onchain-curated playlists with gasless user interaction and proof of curation. Page: https://arcprompts.lovable.app/ideas/music-curated-sound-trails-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Curated Sound Trails" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create and share onchain-curated playlists with gasless user interaction and proof of curation. Discipline: Music & Sound Design (music curation). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored tx allow playlist creation and endorsements without gas burdening fans. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CuratedSoundTrails.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogCuratedSoundTrails /// @notice Create and share onchain-curated playlists with gasless user interaction and proof of curation. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogCuratedSoundTrails { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CuratedSoundTrails` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CuratedSoundTrails.sol CuratedSoundTrails` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every music curation action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-decentralized-jam-sessions-11 · music · Decentralized Jam Sessions ===== Pitch: Record and timestamp decentralized jam sessions to prove creative input for all participants. Page: https://arcprompts.lovable.app/ideas/music-decentralized-jam-sessions-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Decentralized Jam Sessions" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record and timestamp decentralized jam sessions to prove creative input for all participants. Discipline: Music & Sound Design (remote collaboration). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide transparent, shared ownership and contribution logs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DecentralizedJamSessions.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DecentralizedJamSessions /// @notice Record and timestamp decentralized jam sessions to prove creative input for all participants. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DecentralizedJamSessions { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DecentralizedJamSessions` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DecentralizedJamSessions.sol DecentralizedJamSessions` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a remote collaboration action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-decentralized-music-feedback-17 · music · Decentralized Music Feedback ===== Pitch: Collect transparent, timestamped peer reviews for music tracks to build trust and credibility. Page: https://arcprompts.lovable.app/ideas/music-decentralized-music-feedback-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Decentralized Music Feedback" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Collect transparent, timestamped peer reviews for music tracks to build trust and credibility. Discipline: Music & Sound Design (peer review). Onchain primitive: Arc smart contract. Why this primitive: Onchain feedback prevents censorship and alteration of critiques. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DecentralizedMusicFeedback.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DecentralizedMusicFeedback /// @notice Collect transparent, timestamped peer reviews for music tracks to build trust and credibility. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DecentralizedMusicFeedback { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DecentralizedMusicFeedback` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DecentralizedMusicFeedback.sol DecentralizedMusicFeedback` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a peer review action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-decentralized-sound-workshops-22 · music · Decentralized Sound Workshops ===== Pitch: Host interactive sound design workshops with onchain attendance proofs and no gas fees. Page: https://arcprompts.lovable.app/ideas/music-decentralized-sound-workshops-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Decentralized Sound Workshops" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Host interactive sound design workshops with onchain attendance proofs and no gas fees. Discipline: Music & Sound Design (music education). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets streamline user authentication, sponsored tx cover transaction costs gasless. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DecentralizedSoundWorkshops.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogDecentralizedSoundWorkshops /// @notice Host interactive sound design workshops with onchain attendance proofs and no gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogDecentralizedSoundWorkshops { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DecentralizedSoundWorkshops` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DecentralizedSoundWorkshops.sol DecentralizedSoundWorkshops` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every music education action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-dj-set-provenance-19 · music · DJ Set Provenance ===== Pitch: Create certified NFTs of live DJ sets proving originality and performance dates. Page: https://arcprompts.lovable.app/ideas/music-dj-set-provenance-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "DJ Set Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create certified NFTs of live DJ sets proving originality and performance dates. Discipline: Music & Sound Design (live set archiving). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens immutably link to archived set recordings on IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DJSetProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title DJSetProvenance /// @notice ERC-721 provenance for: Create certified NFTs of live DJ sets proving originality and performance dates. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DJSetProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("DJSetProvenance", "DJSETP") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DJSetProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DJSetProvenance.sol DJSetProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a live set archiving, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-dynamic-scores-22 · music · Dynamic Scores ===== Pitch: Store interactive music scores with embedded sounds permanently for composers and educators. Page: https://arcprompts.lovable.app/ideas/music-dynamic-scores-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dynamic Scores" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store interactive music scores with embedded sounds permanently for composers and educators. Discipline: Music & Sound Design (interactive notation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS hosts interactive score files ensuring persistent availability and integrity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DynamicScores.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentDynamicScores /// @notice Store interactive music scores with embedded sounds permanently for composers and educators. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentDynamicScores { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DynamicScores` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DynamicScores.sol DynamicScores` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a interactive notation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-field-recording-mint-12 · music · Field Recording Mint ===== Pitch: Ownership minting for authentic field recordings archived with verifiable IPFS CIDs. Page: https://arcprompts.lovable.app/ideas/music-field-recording-mint-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Field Recording Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Ownership minting for authentic field recordings archived with verifiable IPFS CIDs. Discipline: Music & Sound Design (ambient sound archives). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance tokens preserve authenticity of raw audio captures. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FieldRecordingMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title FieldRecordingMint /// @notice ERC-721 provenance for: Ownership minting for authentic field recordings archived with verifiable IPFS CIDs. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FieldRecordingMint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("FieldRecordingMint", "FIELDR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FieldRecordingMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FieldRecordingMint.sol FieldRecordingMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a ambient sound archives, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-film-score-provenance-22 · music · Film Score Provenance ===== Pitch: Certify original film scores by minting provenance NFTs tied to IPFS CIDs. Page: https://arcprompts.lovable.app/ideas/music-film-score-provenance-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Film Score Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Certify original film scores by minting provenance NFTs tied to IPFS CIDs. Discipline: Music & Sound Design (cinematic composition). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs provide secure proof of score authorship and licensing. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FilmScoreProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title FilmScoreProvenance /// @notice ERC-721 provenance for: Certify original film scores by minting provenance NFTs tied to IPFS CIDs. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FilmScoreProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("FilmScoreProvenance", "FILMSC") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FilmScoreProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FilmScoreProvenance.sol FilmScoreProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a cinematic composition, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-gas-free-live-sampling-4 · music · Gas-Free Live Sampling ===== Pitch: Capture and share live samples onchain instantly without blockchain transaction fees. Page: https://arcprompts.lovable.app/ideas/music-gas-free-live-sampling-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gas-Free Live Sampling" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Capture and share live samples onchain instantly without blockchain transaction fees. Discipline: Music & Sound Design (live-set sampling). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored tx enable artists to record and distribute samples in real time without cost. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GasFreeLiveSampling.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGasFreeLiveSampling /// @notice Capture and share live samples onchain instantly without blockchain transaction fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGasFreeLiveSampling { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GasFreeLiveSampling` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GasFreeLiveSampling.sol GasFreeLiveSampling` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every live-set sampling action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-gasless-beat-battles-9 · music · Gasless Beat Battles ===== Pitch: Host onchain beat battles allowing creators to compete and vote without paying blockchain fees. Page: https://arcprompts.lovable.app/ideas/music-gasless-beat-battles-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Beat Battles" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Host onchain beat battles allowing creators to compete and vote without paying blockchain fees. Discipline: Music & Sound Design (music competitions). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored transactions and seamless wallets enable mass participation without gas deterrents. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessBeatBattles.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessBeatBattles /// @notice Host onchain beat battles allowing creators to compete and vote without paying blockchain fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessBeatBattles { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessBeatBattles` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessBeatBattles.sol GaslessBeatBattles` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every music competitions action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-gasless-loop-licensing-17 · music · Gasless Loop Licensing ===== Pitch: License loops onchain with frictionless onboarding and no blockchain transaction fees. Page: https://arcprompts.lovable.app/ideas/music-gasless-loop-licensing-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Loop Licensing" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT License loops onchain with frictionless onboarding and no blockchain transaction fees. Discipline: Music & Sound Design (loop rights). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets plus sponsored transactions enable seamless licensing without gas concern. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessLoopLicensing.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessLoopLicensing /// @notice License loops onchain with frictionless onboarding and no blockchain transaction fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessLoopLicensing { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessLoopLicensing` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessLoopLicensing.sol GaslessLoopLicensing` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every loop rights action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-gasless-midi-trading-24 · music · Gasless MIDI Trading ===== Pitch: Trade MIDI files onchain instantly with social login and no blockchain fees. Page: https://arcprompts.lovable.app/ideas/music-gasless-midi-trading-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless MIDI Trading" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Trade MIDI files onchain instantly with social login and no blockchain fees. Discipline: Music & Sound Design (MIDI asset exchange). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets plus sponsored transactions allow frictionless, cost-free MIDI trades. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessMIDITrading.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessMIDITrading /// @notice Trade MIDI files onchain instantly with social login and no blockchain fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessMIDITrading { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessMIDITrading` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessMIDITrading.sol GaslessMIDITrading` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every MIDI asset exchange action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-gasless-remix-exchange-0 · music · Gasless Remix Exchange ===== Pitch: Collaborate seamlessly on remixes without blockchain fees or sign-up friction. Page: https://arcprompts.lovable.app/ideas/music-gasless-remix-exchange-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Remix Exchange" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Collaborate seamlessly on remixes without blockchain fees or sign-up friction. Discipline: Music & Sound Design (collaborative remixing). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored transactions let users remix and share without paying gas or managing wallets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessRemixExchange.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessRemixExchange /// @notice Collaborate seamlessly on remixes without blockchain fees or sign-up friction. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessRemixExchange { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessRemixExchange` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessRemixExchange.sol GaslessRemixExchange` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every collaborative remixing action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-gasless-sample-challenges-21 · music · Gasless Sample Challenges ===== Pitch: Run onchain sample challenge contests with free user participation and wallet sign-in. Page: https://arcprompts.lovable.app/ideas/music-gasless-sample-challenges-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Sample Challenges" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Run onchain sample challenge contests with free user participation and wallet sign-in. Discipline: Music & Sound Design (creative contests). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored transactions remove gas cost barrier, Privy wallets simplify onboarding. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessSampleChallenges.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessSampleChallenges /// @notice Run onchain sample challenge contests with free user participation and wallet sign-in. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessSampleChallenges { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessSampleChallenges` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessSampleChallenges.sol GaslessSampleChallenges` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every creative contests action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-gasless-sample-swap-15 · music · Gasless Sample Swap ===== Pitch: Swap samples peer-to-peer with instant, free onchain transactions and simple social login. Page: https://arcprompts.lovable.app/ideas/music-gasless-sample-swap-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Sample Swap" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Swap samples peer-to-peer with instant, free onchain transactions and simple social login. Discipline: Music & Sound Design (sample exchange). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallets plus sponsored tx enable smooth, fee-free sample trades. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessSampleSwap.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessSampleSwap /// @notice Swap samples peer-to-peer with instant, free onchain transactions and simple social login. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessSampleSwap { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessSampleSwap` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessSampleSwap.sol GaslessSampleSwap` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every sample exchange action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-gasless-sound-design-nft-13 · music · Gasless Sound Design NFT ===== Pitch: Mint and trade unique sound designs onchain with free wallet sign-ins and sponsored tx. Page: https://arcprompts.lovable.app/ideas/music-gasless-sound-design-nft-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Sound Design NFT" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint and trade unique sound designs onchain with free wallet sign-ins and sponsored tx. Discipline: Music & Sound Design (NFT sound assets). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets combined with sponsored tx remove barriers for NFT sound creators and buyers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessSoundDesignNFT.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessSoundDesignNFT /// @notice Mint and trade unique sound designs onchain with free wallet sign-ins and sponsored tx. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessSoundDesignNFT { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessSoundDesignNFT` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessSoundDesignNFT.sol GaslessSoundDesignNFT` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every NFT sound assets action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-gasless-synth-sharing-19 · music · Gasless Synth Sharing ===== Pitch: Share and demo synthesizer presets with gas-free onchain transactions and social wallet integration. Page: https://arcprompts.lovable.app/ideas/music-gasless-synth-sharing-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Synth Sharing" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Share and demo synthesizer presets with gas-free onchain transactions and social wallet integration. Discipline: Music & Sound Design (preset sharing). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallets plus sponsored tx allow easy and free preset transfer. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessSynthSharing.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessSynthSharing /// @notice Share and demo synthesizer presets with gas-free onchain transactions and social wallet integration. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessSynthSharing { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessSynthSharing` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessSynthSharing.sol GaslessSynthSharing` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every preset sharing action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-genre-evolution-tokens-17 · music · Genre Evolution Tokens ===== Pitch: Mint NFTs tagging tracks by genre and evolutionary lineage onchain. Page: https://arcprompts.lovable.app/ideas/music-genre-evolution-tokens-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Genre Evolution Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs tagging tracks by genre and evolutionary lineage onchain. Discipline: Music & Sound Design (music style tracking). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens link tracks to genre metadata stored immutably on IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GenreEvolutionTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title GenreEvolutionTokens /// @notice ERC-721 provenance for: Mint NFTs tagging tracks by genre and evolutionary lineage onchain. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract GenreEvolutionTokens is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("GenreEvolutionTokens", "GENREE") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GenreEvolutionTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GenreEvolutionTokens.sol GenreEvolutionTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a music style tracking, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-genre-mapper-19 · music · Genre Mapper ===== Pitch: Attach permanent genre and style metadata to tracks for better discovery and curation. Page: https://arcprompts.lovable.app/ideas/music-genre-mapper-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Genre Mapper" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Attach permanent genre and style metadata to tracks for better discovery and curation. Discipline: Music & Sound Design (music classification). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS stores genre tags linked to audio files permanently and transparently. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GenreMapper.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentGenreMapper /// @notice Attach permanent genre and style metadata to tracks for better discovery and curation. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentGenreMapper { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GenreMapper` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GenreMapper.sol GenreMapper` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a music classification, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-instant-collab-contracts-12 · music · Instant Collab Contracts ===== Pitch: Create and sign collaboration agreements onchain instantly without gas fees or complex wallet setup. Page: https://arcprompts.lovable.app/ideas/music-instant-collab-contracts-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Instant Collab Contracts" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create and sign collaboration agreements onchain instantly without gas fees or complex wallet setup. Discipline: Music & Sound Design (agreement automation). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallets plus sponsored tx allow swift contract handling with minimal user effort. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InstantCollabContracts.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogInstantCollabContracts /// @notice Create and sign collaboration agreements onchain instantly without gas fees or complex wallet setup. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogInstantCollabContracts { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InstantCollabContracts` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InstantCollabContracts.sol InstantCollabContracts` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every agreement automation action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-instant-royalty-splits-3 · music · Instant Royalty Splits ===== Pitch: Automatically split royalties among collaborators with transparent, gasless onchain transactions. Page: https://arcprompts.lovable.app/ideas/music-instant-royalty-splits-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Instant Royalty Splits" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Automatically split royalties among collaborators with transparent, gasless onchain transactions. Discipline: Music & Sound Design (music rights management). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets enable seamless royalty payments without fees, encouraging fair splits. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InstantRoyaltySplits.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogInstantRoyaltySplits /// @notice Automatically split royalties among collaborators with transparent, gasless onchain transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogInstantRoyaltySplits { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InstantRoyaltySplits` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InstantRoyaltySplits.sol InstantRoyaltySplits` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every music rights management action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-instrument-sample-provenance-24 · music · Instrument Sample Provenance ===== Pitch: Create NFTs proving ownership of original instrument sample recordings. Page: https://arcprompts.lovable.app/ideas/music-instrument-sample-provenance-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Instrument Sample Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create NFTs proving ownership of original instrument sample recordings. Discipline: Music & Sound Design (instrumental sample rights). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance tokens securely link samples to creator IPFS metadata. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InstrumentSampleProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title InstrumentSampleProvenance /// @notice ERC-721 provenance for: Create NFTs proving ownership of original instrument sample recordings. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract InstrumentSampleProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("InstrumentSampleProvenance", "INSTRU") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InstrumentSampleProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InstrumentSampleProvenance.sol InstrumentSampleProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a instrumental sample rights, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-interactive-audio-nfts-18 · music · Interactive Audio NFTs ===== Pitch: Mint interactive music NFTs with provable ownership and modifiable IPFS content. Page: https://arcprompts.lovable.app/ideas/music-interactive-audio-nfts-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Interactive Audio NFTs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint interactive music NFTs with provable ownership and modifiable IPFS content. Discipline: Music & Sound Design (dynamic sound compositions). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs enable ownership and dynamic linking of mutable audio files. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InteractiveAudioNFTs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title InteractiveAudioNFTs /// @notice ERC-721 provenance for: Mint interactive music NFTs with provable ownership and modifiable IPFS content. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract InteractiveAudioNFTs is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("InteractiveAudioNFTs", "INTERA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InteractiveAudioNFTs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InteractiveAudioNFTs.sol InteractiveAudioNFTs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a dynamic sound compositions, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-interactive-sound-nfts-12 · music · Interactive Sound NFTs ===== Pitch: Create NFTs with interactive sound design that evolves based on user input and onchain data. Page: https://arcprompts.lovable.app/ideas/music-interactive-sound-nfts-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Interactive Sound NFTs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create NFTs with interactive sound design that evolves based on user input and onchain data. Discipline: Music & Sound Design (dynamic audio). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts enable programmable audio behavior tied to token ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InteractiveSoundNFTs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title InteractiveSoundNFTs /// @notice Create NFTs with interactive sound design that evolves based on user input and onchain data. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract InteractiveSoundNFTs { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InteractiveSoundNFTs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InteractiveSoundNFTs.sol InteractiveSoundNFTs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a dynamic audio action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-live-loop-provenance-4 · music · Live Loop Provenance ===== Pitch: Capture and mint unique live-loop sessions for verified creative ownership. Page: https://arcprompts.lovable.app/ideas/music-live-loop-provenance-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Live Loop Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Capture and mint unique live-loop sessions for verified creative ownership. Discipline: Music & Sound Design (live performance loops). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 NFTs timestamp and link to live loop IPFS snapshots immutably. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LiveLoopProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title LiveLoopProvenance /// @notice ERC-721 provenance for: Capture and mint unique live-loop sessions for verified creative ownership. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract LiveLoopProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("LiveLoopProvenance", "LIVELO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LiveLoopProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LiveLoopProvenance.sol LiveLoopProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a live performance loops, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-live-set-proof-16 · music · Live Set Proof ===== Pitch: Proof your live performances onchain with zero gas fees and social wallet ease. Page: https://arcprompts.lovable.app/ideas/music-live-set-proof-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Live Set Proof" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Proof your live performances onchain with zero gas fees and social wallet ease. Discipline: Music & Sound Design (performance verification). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored tx cover onchain proofing costs, Privy wallets simplify authentication. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LiveSetProof.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogLiveSetProof /// @notice Proof your live performances onchain with zero gas fees and social wallet ease. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogLiveSetProof { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LiveSetProof` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LiveSetProof.sol LiveSetProof` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every performance verification action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-live-set-royalty-split-5 · music · Live Set Royalty Split ===== Pitch: Automatically distribute royalty shares from live streamed performances via smart contracts. Page: https://arcprompts.lovable.app/ideas/music-live-set-royalty-split-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Live Set Royalty Split" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Automatically distribute royalty shares from live streamed performances via smart contracts. Discipline: Music & Sound Design (live performance). Onchain primitive: Arc smart contract. Why this primitive: Onchain automation reduces complexity in live earnings distribution. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LiveSetRoyaltySplit.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title LiveSetRoyaltySplit /// @notice Automatically distribute royalty shares from live streamed performances via smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract LiveSetRoyaltySplit { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LiveSetRoyaltySplit` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LiveSetRoyaltySplit.sol LiveSetRoyaltySplit` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a live performance action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-liveloop-archive-17 · music · LiveLoop Archive ===== Pitch: Permanently store live loop sets with detailed metadata for reuse and proof of creation. Page: https://arcprompts.lovable.app/ideas/music-liveloop-archive-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LiveLoop Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Permanently store live loop sets with detailed metadata for reuse and proof of creation. Discipline: Music & Sound Design (live set recording). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS pins large live loop files ensuring permanent access and origin verification. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LiveLoopArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentLiveLoopArchive /// @notice Permanently store live loop sets with detailed metadata for reuse and proof of creation. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentLiveLoopArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LiveLoopArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LiveLoopArchive.sol LiveLoopArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a live set recording, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-loop-provenance-0 · music · Loop Provenance ===== Pitch: Track and verify original creators of music loops and samples to credit and reward authors accurately. Page: https://arcprompts.lovable.app/ideas/music-loop-provenance-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Loop Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track and verify original creators of music loops and samples to credit and reward authors accurately. Discipline: Music & Sound Design (sample tracking). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts provide immutable provenance records for sound samples, preventing forgery. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LoopProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title LoopProvenance /// @notice Track and verify original creators of music loops and samples to credit and reward authors accurately. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract LoopProvenance { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LoopProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LoopProvenance.sol LoopProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a sample tracking action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-loop-provenance-0 · music · Loop Provenance ===== Pitch: Track and verify origin of audio loops to ensure authenticity for musicians and producers. Page: https://arcprompts.lovable.app/ideas/music-loop-provenance-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Loop Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track and verify origin of audio loops to ensure authenticity for musicians and producers. Discipline: Music & Sound Design (sample lineage). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS provides immutable storage of loop metadata proving origin and version history. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LoopProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentLoopProvenance /// @notice Track and verify origin of audio loops to ensure authenticity for musicians and producers. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentLoopProvenance { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LoopProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LoopProvenance.sol LoopProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a sample lineage, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-loopchain-sync-21 · music · LoopChain Sync ===== Pitch: Synchronize loop edits in real-time with permanent version storage on decentralized archive. Page: https://arcprompts.lovable.app/ideas/music-loopchain-sync-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LoopChain Sync" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Synchronize loop edits in real-time with permanent version storage on decentralized archive. Discipline: Music & Sound Design (collaborative looping). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS pins loop versions ensuring immutable history and shared access. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LoopChainSync.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentLoopChainSync /// @notice Synchronize loop edits in real-time with permanent version storage on decentralized archive. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentLoopChainSync { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LoopChainSync` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LoopChainSync.sol LoopChainSync` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a collaborative looping, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-loopswap-12 · music · LoopSwap ===== Pitch: Decentralized platform for permanent loop exchange with verified content provenance. Page: https://arcprompts.lovable.app/ideas/music-loopswap-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LoopSwap" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Decentralized platform for permanent loop exchange with verified content provenance. Discipline: Music & Sound Design (loop exchange). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS pins loops and transaction manifests guaranteeing content immutability. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LoopSwap.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentLoopSwap /// @notice Decentralized platform for permanent loop exchange with verified content provenance. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentLoopSwap { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LoopSwap` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LoopSwap.sol LoopSwap` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a loop exchange, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-mastering-provenance-tags-9 · music · Mastering Provenance Tags ===== Pitch: Mint verifiable NFTs for mastered tracks to certify mastering sources and dates. Page: https://arcprompts.lovable.app/ideas/music-mastering-provenance-tags-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Mastering Provenance Tags" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint verifiable NFTs for mastered tracks to certify mastering sources and dates. Discipline: Music & Sound Design (audio mastering records). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs immutably link to mastered audio files and mastering engineer credentials. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MasteringProvenanceTags.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MasteringProvenanceTags /// @notice ERC-721 provenance for: Mint verifiable NFTs for mastered tracks to certify mastering sources and dates. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MasteringProvenanceTags is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MasteringProvenanceTags", "MASTER") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MasteringProvenanceTags` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MasteringProvenanceTags.sol MasteringProvenanceTags` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a audio mastering records, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-mix-history-4 · music · Mix History ===== Pitch: Track iterative versions of audio mixes ensuring creators can revert and prove mix lineage. Page: https://arcprompts.lovable.app/ideas/music-mix-history-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Mix History" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track iterative versions of audio mixes ensuring creators can revert and prove mix lineage. Discipline: Music & Sound Design (mix versioning). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS pins mix files with immutable version metadata for transparent history. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MixHistory.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMixHistory /// @notice Track iterative versions of audio mixes ensuring creators can revert and prove mix lineage. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMixHistory { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MixHistory` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MixHistory.sol MixHistory` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a mix versioning, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-mix-snapshot-ledger-1 · music · Mix Snapshot Ledger ===== Pitch: Mint unique mix versions so producers prove original mixes over time. Page: https://arcprompts.lovable.app/ideas/music-mix-snapshot-ledger-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Mix Snapshot Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint unique mix versions so producers prove original mixes over time. Discipline: Music & Sound Design (mix version archiving). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens immutably link to mix versions, enabling verifiable mix lineage. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MixSnapshotLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MixSnapshotLedger /// @notice ERC-721 provenance for: Mint unique mix versions so producers prove original mixes over time. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MixSnapshotLedger is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MixSnapshotLedger", "MIXSNA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MixSnapshotLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MixSnapshotLedger.sol MixSnapshotLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a mix version archiving, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-moodsampler-8 · music · MoodSampler ===== Pitch: Attach permanent mood and emotion tags to samples for easy discovery and curation. Page: https://arcprompts.lovable.app/ideas/music-moodsampler-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MoodSampler" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Attach permanent mood and emotion tags to samples for easy discovery and curation. Discipline: Music & Sound Design (emotional tagging). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS stores mood metadata linked with sample files ensuring permanent tagging. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MoodSampler.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMoodSampler /// @notice Attach permanent mood and emotion tags to samples for easy discovery and curation. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMoodSampler { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MoodSampler` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MoodSampler.sol MoodSampler` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a emotional tagging, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-music-education-tokens-15 · music · Music Education Tokens ===== Pitch: Mint unique verified music tutorial NFTs proving instructor originality. Page: https://arcprompts.lovable.app/ideas/music-music-education-tokens-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Music Education Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint unique verified music tutorial NFTs proving instructor originality. Discipline: Music & Sound Design (tutorial authenticity). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance ensures authentic origin and ownership of tutorial content. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MusicEducationTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MusicEducationTokens /// @notice ERC-721 provenance for: Mint unique verified music tutorial NFTs proving instructor originality. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MusicEducationTokens is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MusicEducationTokens", "MUSICE") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MusicEducationTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MusicEducationTokens.sol MusicEducationTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a tutorial authenticity, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-music-metadata-registry-22 · music · Music Metadata Registry ===== Pitch: Create a decentralized registry for verified, searchable music metadata and ownership info. Page: https://arcprompts.lovable.app/ideas/music-music-metadata-registry-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Music Metadata Registry" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create a decentralized registry for verified, searchable music metadata and ownership info. Discipline: Music & Sound Design (metadata management). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts ensure metadata integrity and decentralized availability. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MusicMetadataRegistry.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title MusicMetadataRegistry /// @notice Create a decentralized registry for verified, searchable music metadata and ownership info. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MusicMetadataRegistry { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MusicMetadataRegistry` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MusicMetadataRegistry.sol MusicMetadataRegistry` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a metadata management action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-nft-beat-marketplace-2 · music · NFT Beat Marketplace ===== Pitch: Buy and sell beats with onchain provenance and zero gas fee transactions. Page: https://arcprompts.lovable.app/ideas/music-nft-beat-marketplace-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NFT Beat Marketplace" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Buy and sell beats with onchain provenance and zero gas fee transactions. Discipline: Music & Sound Design (beat selling). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored tx allow beat creators to sell NFTs without buyers worrying about gas costs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NFTBeatMarketplace.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogNFTBeatMarketplace /// @notice Buy and sell beats with onchain provenance and zero gas fee transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogNFTBeatMarketplace { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NFTBeatMarketplace` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NFTBeatMarketplace.sol NFTBeatMarketplace` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every beat selling action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-onchain-audio-stems-15 · music · Onchain Audio Stems ===== Pitch: Distribute song stems with immutable licenses and resale royalty tracking onchain. Page: https://arcprompts.lovable.app/ideas/music-onchain-audio-stems-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Audio Stems" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Distribute song stems with immutable licenses and resale royalty tracking onchain. Discipline: Music & Sound Design (stem distribution). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts ensure transparent stem ownership and usage rights. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainAudioStems.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainAudioStems /// @notice Distribute song stems with immutable licenses and resale royalty tracking onchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainAudioStems { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainAudioStems` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainAudioStems.sol OnchainAudioStems` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a stem distribution action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-onchain-audio-stems-20 · music · Onchain Audio Stems ===== Pitch: Distribute high-quality audio stems with transparent ownership and zero gas fees. Page: https://arcprompts.lovable.app/ideas/music-onchain-audio-stems-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Audio Stems" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Distribute high-quality audio stems with transparent ownership and zero gas fees. Discipline: Music & Sound Design (stem distribution). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets and sponsored tx enable frictionless stem delivery and proof of provenance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainAudioStems.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogOnchainAudioStems /// @notice Distribute high-quality audio stems with transparent ownership and zero gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogOnchainAudioStems { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainAudioStems` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainAudioStems.sol OnchainAudioStems` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every stem distribution action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-onchain-audio-watermarking-24 · music · Onchain Audio Watermarking ===== Pitch: Embed immutable proof of ownership and usage terms in audio files verified via blockchain. Page: https://arcprompts.lovable.app/ideas/music-onchain-audio-watermarking-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Audio Watermarking" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Embed immutable proof of ownership and usage terms in audio files verified via blockchain. Discipline: Music & Sound Design (copyright protection). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide tamper-proof evidence of audio copyright. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainAudioWatermarking.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainAudioWatermarking /// @notice Embed immutable proof of ownership and usage terms in audio files verified via blockchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainAudioWatermarking { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainAudioWatermarking` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainAudioWatermarking.sol OnchainAudioWatermarking` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a copyright protection action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-onchain-dj-set-logs-18 · music · Onchain DJ Set Logs ===== Pitch: Create immutable records of DJ set tracklists and transitions for copyright and reputation. Page: https://arcprompts.lovable.app/ideas/music-onchain-dj-set-logs-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain DJ Set Logs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create immutable records of DJ set tracklists and transitions for copyright and reputation. Discipline: Music & Sound Design (performance archiving). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide verifiable event histories for DJs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainDJSetLogs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainDJSetLogs /// @notice Create immutable records of DJ set tracklists and transitions for copyright and reputation. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainDJSetLogs { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainDJSetLogs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainDJSetLogs.sol OnchainDJSetLogs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a performance archiving action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-onchain-mix-feedback-14 · music · Onchain Mix Feedback ===== Pitch: Receive verified community feedback on mixes with gasless voting and wallet authentication. Page: https://arcprompts.lovable.app/ideas/music-onchain-mix-feedback-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Mix Feedback" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Receive verified community feedback on mixes with gasless voting and wallet authentication. Discipline: Music & Sound Design (mix critique). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored transactions ensure cost-free feedback submission and Privy wallet streamlines sign-in. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainMixFeedback.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogOnchainMixFeedback /// @notice Receive verified community feedback on mixes with gasless voting and wallet authentication. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogOnchainMixFeedback { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainMixFeedback` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainMixFeedback.sol OnchainMixFeedback` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every mix critique action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-onchain-music-curriculum-7 · music · Onchain Music Curriculum ===== Pitch: Certify students’ composition and sound design achievements onchain for transparent credentialing. Page: https://arcprompts.lovable.app/ideas/music-onchain-music-curriculum-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Music Curriculum" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Certify students’ composition and sound design achievements onchain for transparent credentialing. Discipline: Music & Sound Design (music education). Onchain primitive: Arc smart contract. Why this primitive: Immutable record of skill acquisition ensures authenticity in credentials. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainMusicCurriculum.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainMusicCurriculum /// @notice Certify students’ composition and sound design achievements onchain for transparent credentialing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainMusicCurriculum { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainMusicCurriculum` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainMusicCurriculum.sol OnchainMusicCurriculum` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a music education action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-onchain-remix-competitions-13 · music · Onchain Remix Competitions ===== Pitch: Host remix contests with transparent voting and automatic prize payouts on the blockchain. Page: https://arcprompts.lovable.app/ideas/music-onchain-remix-competitions-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Remix Competitions" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Host remix contests with transparent voting and automatic prize payouts on the blockchain. Discipline: Music & Sound Design (contest management). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts enforce fairness and instant distribution of rewards. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainRemixCompetitions.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainRemixCompetitions /// @notice Host remix contests with transparent voting and automatic prize payouts on the blockchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainRemixCompetitions { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainRemixCompetitions` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainRemixCompetitions.sol OnchainRemixCompetitions` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a contest management action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-onchain-remix-rights-1 · music · Onchain Remix Rights ===== Pitch: Enable transparent licensing and royalty splits for remixed tracks directly on the blockchain. Page: https://arcprompts.lovable.app/ideas/music-onchain-remix-rights-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Remix Rights" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable transparent licensing and royalty splits for remixed tracks directly on the blockchain. Discipline: Music & Sound Design (remix licensing). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts automate rights management and payments without intermediaries. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainRemixRights.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainRemixRights /// @notice Enable transparent licensing and royalty splits for remixed tracks directly on the blockchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainRemixRights { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainRemixRights` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainRemixRights.sol OnchainRemixRights` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a remix licensing action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-onchain-sample-swap-4 · music · Onchain Sample Swap ===== Pitch: Facilitate peer-to-peer swapping of music samples with provable ownership and usage rights. Page: https://arcprompts.lovable.app/ideas/music-onchain-sample-swap-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Sample Swap" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Facilitate peer-to-peer swapping of music samples with provable ownership and usage rights. Discipline: Music & Sound Design (sample exchange). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide trustless verification in the exchange process. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainSampleSwap.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainSampleSwap /// @notice Facilitate peer-to-peer swapping of music samples with provable ownership and usage rights. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainSampleSwap { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainSampleSwap` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainSampleSwap.sol OnchainSampleSwap` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a sample exchange action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-onchain-sound-packs-1 · music · Onchain Sound Packs ===== Pitch: Distribute sound packs with verified authenticity and instant user access via gasless wallets. Page: https://arcprompts.lovable.app/ideas/music-onchain-sound-packs-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Sound Packs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Distribute sound packs with verified authenticity and instant user access via gasless wallets. Discipline: Music & Sound Design (sample library distribution). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social wallets enable frictionless user onboarding and free samples delivery onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainSoundPacks.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogOnchainSoundPacks /// @notice Distribute sound packs with verified authenticity and instant user access via gasless wallets. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogOnchainSoundPacks { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainSoundPacks` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainSoundPacks.sol OnchainSoundPacks` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every sample library distribution action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-onchain-synth-presets-10 · music · Onchain Synth Presets ===== Pitch: Distribute unique synthesizer presets as verifiable NFTs with built-in royalty logic. Page: https://arcprompts.lovable.app/ideas/music-onchain-synth-presets-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Synth Presets" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Distribute unique synthesizer presets as verifiable NFTs with built-in royalty logic. Discipline: Music & Sound Design (synth programming). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts guarantee authenticity and automate resale royalties. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainSynthPresets.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainSynthPresets /// @notice Distribute unique synthesizer presets as verifiable NFTs with built-in royalty logic. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainSynthPresets { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainSynthPresets` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainSynthPresets.sol OnchainSynthPresets` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a synth programming action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-onchain-synth-presets-8 · music · Onchain Synth Presets ===== Pitch: Distribute and monetize synth presets with gas-free transactions and easy social login. Page: https://arcprompts.lovable.app/ideas/music-onchain-synth-presets-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Synth Presets" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Distribute and monetize synth presets with gas-free transactions and easy social login. Discipline: Music & Sound Design (sound design presets). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallets facilitate frictionless preset purchase and sharing without gas fees. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainSynthPresets.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogOnchainSynthPresets /// @notice Distribute and monetize synth presets with gas-free transactions and easy social login. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogOnchainSynthPresets { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainSynthPresets` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainSynthPresets.sol OnchainSynthPresets` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every sound design presets action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-onchain-vocal-processing-21 · music · Onchain Vocal Processing ===== Pitch: Store and verify customizable vocal effect presets and chains onchain for sharing and licensing. Page: https://arcprompts.lovable.app/ideas/music-onchain-vocal-processing-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Vocal Processing" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store and verify customizable vocal effect presets and chains onchain for sharing and licensing. Discipline: Music & Sound Design (vocal effects). Onchain primitive: Arc smart contract. Why this primitive: Immutable contracts authenticate effect creations and usage rights. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainVocalProcessing.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainVocalProcessing /// @notice Store and verify customizable vocal effect presets and chains onchain for sharing and licensing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainVocalProcessing { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainVocalProcessing` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainVocalProcessing.sol OnchainVocalProcessing` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a vocal effects action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-patch-ledger-13 · music · Patch Ledger ===== Pitch: Record creation and usage history of sound patches to prove originality and licensing. Page: https://arcprompts.lovable.app/ideas/music-patch-ledger-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Patch Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record creation and usage history of sound patches to prove originality and licensing. Discipline: Music & Sound Design (sound patch tracking). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS stores patches and metadata ensuring unchangeable usage logs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PatchLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPatchLedger /// @notice Record creation and usage history of sound patches to prove originality and licensing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPatchLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PatchLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PatchLedger.sol PatchLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a sound patch tracking, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-patch-vault-1 · music · Patch Vault ===== Pitch: Securely store and share synthesizer presets with verified authenticity and version control. Page: https://arcprompts.lovable.app/ideas/music-patch-vault-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Patch Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely store and share synthesizer presets with verified authenticity and version control. Discipline: Music & Sound Design (synth preset archiving). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata IPFS immutably hosts preset files and metadata accessible globally. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PatchVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPatchVault /// @notice Securely store and share synthesizer presets with verified authenticity and version control. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPatchVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PatchVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PatchVault.sol PatchVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a synth preset archiving, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-podcast-sound-mint-21 · music · Podcast Sound Mint ===== Pitch: Mint provenance NFTs for original podcast episodes guaranteeing ownership. Page: https://arcprompts.lovable.app/ideas/music-podcast-sound-mint-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Podcast Sound Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint provenance NFTs for original podcast episodes guaranteeing ownership. Discipline: Music & Sound Design (podcast audio authenticity). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens link episodes immutably with metadata on IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PodcastSoundMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PodcastSoundMint /// @notice ERC-721 provenance for: Mint provenance NFTs for original podcast episodes guaranteeing ownership. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PodcastSoundMint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PodcastSoundMint", "PODCAS") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PodcastSoundMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PodcastSoundMint.sol PodcastSoundMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a podcast audio authenticity, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-preset-gallery-6 · music · Preset Gallery ===== Pitch: Create a decentralized gallery of verified plugin presets with community ratings. Page: https://arcprompts.lovable.app/ideas/music-preset-gallery-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Preset Gallery" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create a decentralized gallery of verified plugin presets with community ratings. Discipline: Music & Sound Design (plugin preset sharing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata IPFS hosts presets guaranteeing tamper-proof sharing and retrieval. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PresetGallery.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPresetGallery /// @notice Create a decentralized gallery of verified plugin presets with community ratings. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPresetGallery { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PresetGallery` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PresetGallery.sol PresetGallery` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a plugin preset sharing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-remix-chain-registry-10 · music · Remix Chain Registry ===== Pitch: Trace remix origins onchain by minting NFT tokens for each remix iteration. Page: https://arcprompts.lovable.app/ideas/music-remix-chain-registry-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Remix Chain Registry" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Trace remix origins onchain by minting NFT tokens for each remix iteration. Discipline: Music & Sound Design (remix lineage tracking). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens create transparent, provable remix trees with IPFS metadata. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/RemixChainRegistry.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title RemixChainRegistry /// @notice ERC-721 provenance for: Trace remix origins onchain by minting NFT tokens for each remix iteration. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract RemixChainRegistry is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("RemixChainRegistry", "REMIXC") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs RemixChainRegistry` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/RemixChainRegistry.sol RemixChainRegistry` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a remix lineage tracking, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-remix-rights-11 · music · Remix Rights ===== Pitch: Track remix permissions and derivatives with permanent records linked to original works. Page: https://arcprompts.lovable.app/ideas/music-remix-rights-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Remix Rights" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track remix permissions and derivatives with permanent records linked to original works. Discipline: Music & Sound Design (remix management). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS stores remix manifests ensuring immutable rights documentation. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/RemixRights.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentRemixRights /// @notice Track remix permissions and derivatives with permanent records linked to original works. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentRemixRights { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs RemixRights` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/RemixRights.sol RemixRights` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a remix management, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-sample-chain-marketplace-14 · music · Sample Chain Marketplace ===== Pitch: Create a decentralized marketplace for verified, licensed music samples with automatic royalties. Page: https://arcprompts.lovable.app/ideas/music-sample-chain-marketplace-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sample Chain Marketplace" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create a decentralized marketplace for verified, licensed music samples with automatic royalties. Discipline: Music & Sound Design (music marketplaces). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts reduce intermediaries and enforce payment distribution. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SampleChainMarketplace.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SampleChainMarketplace /// @notice Create a decentralized marketplace for verified, licensed music samples with automatic royalties. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SampleChainMarketplace { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SampleChainMarketplace` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SampleChainMarketplace.sol SampleChainMarketplace` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a music marketplaces action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-sample-provenance-vault-0 · music · Sample Provenance Vault ===== Pitch: Securely mint and verify original samples for trusted reuse and licensing. Page: https://arcprompts.lovable.app/ideas/music-sample-provenance-vault-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sample Provenance Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely mint and verify original samples for trusted reuse and licensing. Discipline: Music & Sound Design (sample library curation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance ensures unalterable origin tracking for authentic sample ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SampleProvenanceVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SampleProvenanceVault /// @notice ERC-721 provenance for: Securely mint and verify original samples for trusted reuse and licensing. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SampleProvenanceVault is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SampleProvenanceVault", "SAMPLE") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SampleProvenanceVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SampleProvenanceVault.sol SampleProvenanceVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a sample library curation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-sample-remix-rights-3 · music · Sample Remix Rights ===== Pitch: Mint remixable sample NFTs granting verified usage and royalty rights to remixers. Page: https://arcprompts.lovable.app/ideas/music-sample-remix-rights-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sample Remix Rights" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint remixable sample NFTs granting verified usage and royalty rights to remixers. Discipline: Music & Sound Design (remix licensing). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain NFT ownership proves and transfers remix rights transparently. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SampleRemixRights.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SampleRemixRights /// @notice ERC-721 provenance for: Mint remixable sample NFTs granting verified usage and royalty rights to remixers. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SampleRemixRights is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SampleRemixRights", "SAMPLE") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SampleRemixRights` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SampleRemixRights.sol SampleRemixRights` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a remix licensing, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-samplechain-3 · music · SampleChain ===== Pitch: Manage and prove sample license ownership with permanent onchain records for producers. Page: https://arcprompts.lovable.app/ideas/music-samplechain-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SampleChain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Manage and prove sample license ownership with permanent onchain records for producers. Discipline: Music & Sound Design (sample licensing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata IPFS stores license documents and metadata linked to sample CIDs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SampleChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentSampleChain /// @notice Manage and prove sample license ownership with permanent onchain records for producers. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentSampleChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SampleChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SampleChain.sol SampleChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a sample licensing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-samplechain-pro-16 · music · SampleChain Pro ===== Pitch: Professional tool to trace sample origins and licensing history with permanent records. Page: https://arcprompts.lovable.app/ideas/music-samplechain-pro-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SampleChain Pro" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Professional tool to trace sample origins and licensing history with permanent records. Discipline: Music & Sound Design (sample provenance). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS hosts sample metadata and provenance documents immutably and transparently. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SampleChainPro.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentSampleChainPro /// @notice Professional tool to trace sample origins and licensing history with permanent records. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentSampleChainPro { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SampleChainPro` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SampleChainPro.sol SampleChainPro` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a sample provenance, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-score-archive-2 · music · Score Archive ===== Pitch: Permanently archive music scores and compositions with guaranteed timestamped records. Page: https://arcprompts.lovable.app/ideas/music-score-archive-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Score Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Permanently archive music scores and compositions with guaranteed timestamped records. Discipline: Music & Sound Design (composition notation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS storage ensures unalterable score files accessible forever. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScoreArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentScoreArchive /// @notice Permanently archive music scores and compositions with guaranteed timestamped records. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentScoreArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScoreArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScoreArchive.sol ScoreArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a composition notation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-smart-contract-audio-rng-19 · music · Smart Contract Audio RNG ===== Pitch: Use onchain randomness to generate unique, verifiable generative music pieces as NFTs. Page: https://arcprompts.lovable.app/ideas/music-smart-contract-audio-rng-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Smart Contract Audio RNG" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Use onchain randomness to generate unique, verifiable generative music pieces as NFTs. Discipline: Music & Sound Design (generative music). Onchain primitive: Arc smart contract. Why this primitive: Sepolia’s secure RNG enables trustable generative outputs onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SmartContractAudioRNG.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SmartContractAudioRNG /// @notice Use onchain randomness to generate unique, verifiable generative music pieces as NFTs. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SmartContractAudioRNG { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SmartContractAudioRNG` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SmartContractAudioRNG.sol SmartContractAudioRNG` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a generative music action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-smart-contract-sync-licenses-23 · music · Smart Contract Sync Licenses ===== Pitch: Automate sync rights clearance and royalty splits for film, TV, and ads on the blockchain. Page: https://arcprompts.lovable.app/ideas/music-smart-contract-sync-licenses-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Smart Contract Sync Licenses" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Automate sync rights clearance and royalty splits for film, TV, and ads on the blockchain. Discipline: Music & Sound Design (sync licensing). Onchain primitive: Arc smart contract. Why this primitive: Onchain contracts reduce delays and disputes in sync licensing. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SmartContractSyncLicenses.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SmartContractSyncLicenses /// @notice Automate sync rights clearance and royalty splits for film, TV, and ads on the blockchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SmartContractSyncLicenses { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SmartContractSyncLicenses` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SmartContractSyncLicenses.sol SmartContractSyncLicenses` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a sync licensing action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-social-audio-credits-18 · music · Social Audio Credits ===== Pitch: Tip and credit collaborators instantly with gasless onchain microtransactions via social login. Page: https://arcprompts.lovable.app/ideas/music-social-audio-credits-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Social Audio Credits" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Tip and credit collaborators instantly with gasless onchain microtransactions via social login. Discipline: Music & Sound Design (microtransactions). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored tx reduce friction in micro-payments and Privy wallets ease user adoption. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SocialAudioCredits.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSocialAudioCredits /// @notice Tip and credit collaborators instantly with gasless onchain microtransactions via social login. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSocialAudioCredits { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SocialAudioCredits` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SocialAudioCredits.sol SocialAudioCredits` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every microtransactions action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-social-jam-sessions-5 · music · Social Jam Sessions ===== Pitch: Join real-time jam sessions with authenticated users and transparent blockchain session logs. Page: https://arcprompts.lovable.app/ideas/music-social-jam-sessions-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Social Jam Sessions" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Join real-time jam sessions with authenticated users and transparent blockchain session logs. Discipline: Music & Sound Design (online collaboration). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social wallets simplify user sign-in and gasless session synchronization. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SocialJamSessions.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSocialJamSessions /// @notice Join real-time jam sessions with authenticated users and transparent blockchain session logs. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSocialJamSessions { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SocialJamSessions` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SocialJamSessions.sol SocialJamSessions` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every online collaboration action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-songwriting-drafts-mint-11 · music · Songwriting Drafts Mint ===== Pitch: Mint NFTs for songwriting drafts to prove original lyric authorship and evolution. Page: https://arcprompts.lovable.app/ideas/music-songwriting-drafts-mint-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Songwriting Drafts Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs for songwriting drafts to prove original lyric authorship and evolution. Discipline: Music & Sound Design (lyric version control). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs timestamp and store lyrical versions securely on IPFS with provenance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SongwritingDraftsMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SongwritingDraftsMint /// @notice ERC-721 provenance for: Mint NFTs for songwriting drafts to prove original lyric authorship and evolution. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SongwritingDraftsMint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SongwritingDraftsMint", "SONGWR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SongwritingDraftsMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SongwritingDraftsMint.sol SongwritingDraftsMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a lyric version control, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-sound-asset-crowdfunding-9 · music · Sound Asset Crowdfunding ===== Pitch: Raise decentralized funding for music and sound projects with onchain milestone payments. Page: https://arcprompts.lovable.app/ideas/music-sound-asset-crowdfunding-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sound Asset Crowdfunding" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Raise decentralized funding for music and sound projects with onchain milestone payments. Discipline: Music & Sound Design (music funding). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts automate fund release upon achievement of creative goals. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SoundAssetCrowdfunding.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SoundAssetCrowdfunding /// @notice Raise decentralized funding for music and sound projects with onchain milestone payments. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SoundAssetCrowdfunding { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SoundAssetCrowdfunding` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SoundAssetCrowdfunding.sol SoundAssetCrowdfunding` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a music funding action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-sound-asset-licensing-16 · music · Sound Asset Licensing ===== Pitch: Streamline sound asset licensing with minting provable NFT ownership tokens. Page: https://arcprompts.lovable.app/ideas/music-sound-asset-licensing-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sound Asset Licensing" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Streamline sound asset licensing with minting provable NFT ownership tokens. Discipline: Music & Sound Design (asset marketplace). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain NFTs verify licenses and enable transparent royalty tracking. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SoundAssetLicensing.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SoundAssetLicensing /// @notice ERC-721 provenance for: Streamline sound asset licensing with minting provable NFT ownership tokens. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SoundAssetLicensing is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SoundAssetLicensing", "SOUNDA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SoundAssetLicensing` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SoundAssetLicensing.sol SoundAssetLicensing` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a asset marketplace, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-sound-design-presets-13 · music · Sound Design Presets ===== Pitch: Mint and trade unique sound design effect presets with creator provenance. Page: https://arcprompts.lovable.app/ideas/music-sound-design-presets-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sound Design Presets" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint and trade unique sound design effect presets with creator provenance. Discipline: Music & Sound Design (fx preset marketplaces). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens link presets to creators, ensuring authenticity and resale. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SoundDesignPresets.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SoundDesignPresets /// @notice ERC-721 provenance for: Mint and trade unique sound design effect presets with creator provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SoundDesignPresets is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SoundDesignPresets", "SOUNDD") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SoundDesignPresets` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SoundDesignPresets.sol SoundDesignPresets` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a fx preset marketplaces, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-sound-effect-nft-vault-3 · music · Sound Effect NFT Vault ===== Pitch: Mint and trade exclusive sound effects as NFTs ensuring ownership and monetization for creators. Page: https://arcprompts.lovable.app/ideas/music-sound-effect-nft-vault-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sound Effect NFT Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint and trade exclusive sound effects as NFTs ensuring ownership and monetization for creators. Discipline: Music & Sound Design (sound libraries). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts enable unique, verifiable ownership of sound assets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SoundEffectNFTVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SoundEffectNFTVault /// @notice Mint and trade exclusive sound effects as NFTs ensuring ownership and monetization for creators. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SoundEffectNFTVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SoundEffectNFTVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SoundEffectNFTVault.sol SoundEffectNFTVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a sound libraries action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-sound-effect-provenance-6 · music · Sound Effect Provenance ===== Pitch: Identify and authenticate original sound effect creators with minting onchain tokens. Page: https://arcprompts.lovable.app/ideas/music-sound-effect-provenance-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sound Effect Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Identify and authenticate original sound effect creators with minting onchain tokens. Discipline: Music & Sound Design (sound effect libraries). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Immutable token metadata links sound effects to verified creators. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SoundEffectProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SoundEffectProvenance /// @notice ERC-721 provenance for: Identify and authenticate original sound effect creators with minting onchain tokens. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SoundEffectProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SoundEffectProvenance", "SOUNDE") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SoundEffectProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SoundEffectProvenance.sol SoundEffectProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a sound effect libraries, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-sound-nft-15 · music · Sound NFT ===== Pitch: Create permanent and verifiable audio NFTs with linked metadata on decentralized storage. Page: https://arcprompts.lovable.app/ideas/music-sound-nft-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sound NFT" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create permanent and verifiable audio NFTs with linked metadata on decentralized storage. Discipline: Music & Sound Design (audio collectibles). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata IPFS ensures NFT audio assets remain immutable and accessible. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SoundNFT.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentSoundNFT /// @notice Create permanent and verifiable audio NFTs with linked metadata on decentralized storage. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentSoundNFT { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SoundNFT` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SoundNFT.sol SoundNFT` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a audio collectibles, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-soundpatch-market-18 · music · SoundPatch Market ===== Pitch: Decentralized marketplace for buying and selling sound patches with permanent proof of originality. Page: https://arcprompts.lovable.app/ideas/music-soundpatch-market-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SoundPatch Market" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Decentralized marketplace for buying and selling sound patches with permanent proof of originality. Discipline: Music & Sound Design (preset marketplace). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata IPFS stores patch files and transaction history immutably. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SoundPatchMarket.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentSoundPatchMarket /// @notice Decentralized marketplace for buying and selling sound patches with permanent proof of originality. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentSoundPatchMarket { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SoundPatchMarket` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SoundPatchMarket.sol SoundPatchMarket` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a preset marketplace, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-soundscape-atlas-5 · music · Soundscape Atlas ===== Pitch: Catalog and permanently archive environmental soundscapes for reuse and licensing. Page: https://arcprompts.lovable.app/ideas/music-soundscape-atlas-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Soundscape Atlas" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Catalog and permanently archive environmental soundscapes for reuse and licensing. Discipline: Music & Sound Design (field recording). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS stores large audio files and metadata ensuring long-term access and provenance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SoundscapeAtlas.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentSoundscapeAtlas /// @notice Catalog and permanently archive environmental soundscapes for reuse and licensing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentSoundscapeAtlas { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SoundscapeAtlas` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SoundscapeAtlas.sol SoundscapeAtlas` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a field recording, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-stem-provenance-23 · music · Stem Provenance ===== Pitch: Track origin and licensing of individual stems with permanent onchain metadata storage. Page: https://arcprompts.lovable.app/ideas/music-stem-provenance-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Stem Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track origin and licensing of individual stems with permanent onchain metadata storage. Discipline: Music & Sound Design (multitrack tracking). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata IPFS immutably stores stem files and associated licensing info. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StemProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentStemProvenance /// @notice Track origin and licensing of individual stems with permanent onchain metadata storage. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentStemProvenance { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StemProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StemProvenance.sol StemProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a multitrack tracking, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-synth-patch-provenance-2 · music · Synth Patch Provenance ===== Pitch: Create certified original synth presets with owned provenance for resale or sharing. Page: https://arcprompts.lovable.app/ideas/music-synth-patch-provenance-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Synth Patch Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create certified original synth presets with owned provenance for resale or sharing. Discipline: Music & Sound Design (synthesizer preset sharing). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs verify authorship of preset files stored off-chain on IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SynthPatchProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SynthPatchProvenance /// @notice ERC-721 provenance for: Create certified original synth presets with owned provenance for resale or sharing. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SynthPatchProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SynthPatchProvenance", "SYNTHP") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SynthPatchProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SynthPatchProvenance.sol SynthPatchProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a synthesizer preset sharing, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-synth-patchbook-10 · music · Synth Patchbook ===== Pitch: Create a permanent catalog of unique synth patches for community sharing and licensing. Page: https://arcprompts.lovable.app/ideas/music-synth-patchbook-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Synth Patchbook" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create a permanent catalog of unique synth patches for community sharing and licensing. Discipline: Music & Sound Design (sound design catalog). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata IPFS hosts patch files and metadata ensuring authenticity and longevity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SynthPatchbook.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentSynthPatchbook /// @notice Create a permanent catalog of unique synth patches for community sharing and licensing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentSynthPatchbook { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SynthPatchbook` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SynthPatchbook.sol SynthPatchbook` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a sound design catalog, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-token-gated-music-releases-20 · music · Token-Gated Music Releases ===== Pitch: Release exclusive music content accessible only to token holders via smart contracts. Page: https://arcprompts.lovable.app/ideas/music-token-gated-music-releases-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Token-Gated Music Releases" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Release exclusive music content accessible only to token holders via smart contracts. Discipline: Music & Sound Design (exclusive content). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts manage access rights and track exclusivity automatically. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TokenGatedMusicReleases.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title TokenGatedMusicReleases /// @notice Release exclusive music content accessible only to token holders via smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TokenGatedMusicReleases { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TokenGatedMusicReleases` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TokenGatedMusicReleases.sol TokenGatedMusicReleases` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a exclusive content action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-tokenized-composer-credits-8 · music · Tokenized Composer Credits ===== Pitch: Issue tokens representing composer credits to ensure fair recognition and revenue sharing. Page: https://arcprompts.lovable.app/ideas/music-tokenized-composer-credits-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Tokenized Composer Credits" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Issue tokens representing composer credits to ensure fair recognition and revenue sharing. Discipline: Music & Sound Design (credit attribution). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts make composer credit tamper-proof and tradable. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TokenizedComposerCredits.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title TokenizedComposerCredits /// @notice Issue tokens representing composer credits to ensure fair recognition and revenue sharing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TokenizedComposerCredits { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TokenizedComposerCredits` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TokenizedComposerCredits.sol TokenizedComposerCredits` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a credit attribution action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-transparent-sample-licensing-11 · music · Transparent Sample Licensing ===== Pitch: License samples with onchain transparency and gasless user adoption via social login. Page: https://arcprompts.lovable.app/ideas/music-transparent-sample-licensing-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Transparent Sample Licensing" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT License samples with onchain transparency and gasless user adoption via social login. Discipline: Music & Sound Design (sample rights). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social wallets reduce onboarding friction, sponsored tx cover license actions gas-free. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TransparentSampleLicensing.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogTransparentSampleLicensing /// @notice License samples with onchain transparency and gasless user adoption via social login. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogTransparentSampleLicensing { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TransparentSampleLicensing` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TransparentSampleLicensing.sol TransparentSampleLicensing` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every sample rights action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-vinyl-metadata-7 · music · Vinyl Metadata ===== Pitch: Digitally archive vinyl metadata linked to physical records for collectors and sellers. Page: https://arcprompts.lovable.app/ideas/music-vinyl-metadata-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Vinyl Metadata" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Digitally archive vinyl metadata linked to physical records for collectors and sellers. Discipline: Music & Sound Design (record collection). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS stores immutable record metadata accessible worldwide and permanently. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VinylMetadata.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentVinylMetadata /// @notice Digitally archive vinyl metadata linked to physical records for collectors and sellers. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentVinylMetadata { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VinylMetadata` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VinylMetadata.sol VinylMetadata` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a record collection, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-virtual-instrument-tokens-5 · music · Virtual Instrument Tokens ===== Pitch: Mint exclusive sample pack NFTs with creator ownership and usage proofs. Page: https://arcprompts.lovable.app/ideas/music-virtual-instrument-tokens-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Virtual Instrument Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint exclusive sample pack NFTs with creator ownership and usage proofs. Discipline: Music & Sound Design (instrument sample packs). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs provide authenticated proof of creation and distribution for sample packs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VirtualInstrumentTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title VirtualInstrumentTokens /// @notice ERC-721 provenance for: Mint exclusive sample pack NFTs with creator ownership and usage proofs. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VirtualInstrumentTokens is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("VirtualInstrumentTokens", "VIRTUA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VirtualInstrumentTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VirtualInstrumentTokens.sol VirtualInstrumentTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a instrument sample packs, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA music-voice-sample-tokens-20 · music · Voice Sample Tokens ===== Pitch: Mint NFTs certifying original vocal samples for reuse and licensing. Page: https://arcprompts.lovable.app/ideas/music-voice-sample-tokens-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Voice Sample Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs certifying original vocal samples for reuse and licensing. Discipline: Music & Sound Design (vocal sample authentication). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance ensures immutable vocal sample creator attribution. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VoiceSampleTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title VoiceSampleTokens /// @notice ERC-721 provenance for: Mint NFTs certifying original vocal samples for reuse and licensing. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VoiceSampleTokens is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("VoiceSampleTokens", "VOICES") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VoiceSampleTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VoiceSampleTokens.sol VoiceSampleTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a vocal sample authentication, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-360proof-chain-17 · photography · 360Proof Chain ===== Pitch: Authenticate and secure ownership of 360-degree photos on the blockchain. Page: https://arcprompts.lovable.app/ideas/photography-360proof-chain-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "360Proof Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate and secure ownership of 360-degree photos on the blockchain. Discipline: Photography (360° photography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens ensure tamper-proof provenance for 360° IPFS photo assets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/360ProofChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title 360ProofChain /// @notice ERC-721 provenance for: Authenticate and secure ownership of 360-degree photos on the blockchain. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract 360ProofChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("360ProofChain", "360PRO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs 360ProofChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/360ProofChain.sol 360ProofChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a 360° photography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-batchpin-manager-23 · photography · BatchPin Manager ===== Pitch: Pin and manage large photo batches on IPFS with JWT to streamline permanent storage workflows. Page: https://arcprompts.lovable.app/ideas/photography-batchpin-manager-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "BatchPin Manager" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin and manage large photo batches on IPFS with JWT to streamline permanent storage workflows. Discipline: Photography (bulk image processing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT supports bulk pinning with reliable CID management for large photo sets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/BatchPinManager.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentBatchPinManager /// @notice Pin and manage large photo batches on IPFS with JWT to streamline permanent storage workflows. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentBatchPinManager { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs BatchPinManager` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/BatchPinManager.sol BatchPinManager` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a bulk image processing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-chain-backed-photo-grants-19 · photography · Chain-Backed Photo Grants ===== Pitch: Manage and distribute photography grant funds transparently on Sepolia smart contracts. Page: https://arcprompts.lovable.app/ideas/photography-chain-backed-photo-grants-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Chain-Backed Photo Grants" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Manage and distribute photography grant funds transparently on Sepolia smart contracts. Discipline: Photography (funding management). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts ensure fair, traceable, and automated grant distribution. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChainBackedPhotoGrants.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ChainBackedPhotoGrants /// @notice Manage and distribute photography grant funds transparently on Sepolia smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ChainBackedPhotoGrants { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChainBackedPhotoGrants` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChainBackedPhotoGrants.sol ChainBackedPhotoGrants` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a funding management action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-chain-based-mentorship-22 · photography · Chain-based Mentorship ===== Pitch: Match photography mentors and mentees with verified contracts and session records onchain. Page: https://arcprompts.lovable.app/ideas/photography-chain-based-mentorship-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Chain-based Mentorship" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Match photography mentors and mentees with verified contracts and session records onchain. Discipline: Photography (educational matching). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts track commitments and ensure trust between mentors and learners. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChainbasedMentorship.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ChainbasedMentorship /// @notice Match photography mentors and mentees with verified contracts and session records onchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ChainbasedMentorship { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChainbasedMentorship` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChainbasedMentorship.sol ChainbasedMentorship` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a educational matching action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-chain-linked-exif-15 · photography · Chain-Linked EXIF ===== Pitch: Anchor and verify photo EXIF metadata on Sepolia to prevent tampering and ensure trust. Page: https://arcprompts.lovable.app/ideas/photography-chain-linked-exif-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Chain-Linked EXIF" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Anchor and verify photo EXIF metadata on Sepolia to prevent tampering and ensure trust. Discipline: Photography (metadata anchoring). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts store immutable metadata fingerprints for reliable verification. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChainLinkedEXIF.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ChainLinkedEXIF /// @notice Anchor and verify photo EXIF metadata on Sepolia to prevent tampering and ensure trust. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ChainLinkedEXIF { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChainLinkedEXIF` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChainLinkedEXIF.sol ChainLinkedEXIF` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a metadata anchoring action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-chain-linked-portfolios-23 · photography · Chain-Linked Portfolios ===== Pitch: Create gasless onchain portfolios linking proofs and metadata automatically. Page: https://arcprompts.lovable.app/ideas/photography-chain-linked-portfolios-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Chain-Linked Portfolios" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create gasless onchain portfolios linking proofs and metadata automatically. Discipline: Photography (portfolio management). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social and sponsored tx foster effortless portfolio updates. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChainLinkedPortfolios.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogChainLinkedPortfolios /// @notice Create gasless onchain portfolios linking proofs and metadata automatically. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogChainLinkedPortfolios { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChainLinkedPortfolios` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChainLinkedPortfolios.sol ChainLinkedPortfolios` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every portfolio management action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-chain-provenance-logs-11 · photography · Chain Provenance Logs ===== Pitch: Track entire lifecycle and ownership history of photos on Sepolia for provenance assurance. Page: https://arcprompts.lovable.app/ideas/photography-chain-provenance-logs-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Chain Provenance Logs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track entire lifecycle and ownership history of photos on Sepolia for provenance assurance. Discipline: Photography (provenance tracking). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts provide an immutable chain of custody for photos. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChainProvenanceLogs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ChainProvenanceLogs /// @notice Track entire lifecycle and ownership history of photos on Sepolia for provenance assurance. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ChainProvenanceLogs { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChainProvenanceLogs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChainProvenanceLogs.sol ChainProvenanceLogs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a provenance tracking action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-chain-stamped-edits-13 · photography · Chain-Stamped Edits ===== Pitch: Record photo edit history onchain instantly without needing users to pay gas. Page: https://arcprompts.lovable.app/ideas/photography-chain-stamped-edits-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Chain-Stamped Edits" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record photo edit history onchain instantly without needing users to pay gas. Discipline: Photography (edit provenance). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets plus sponsored tx enable seamless edit provenance tracking. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChainStampedEdits.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogChainStampedEdits /// @notice Record photo edit history onchain instantly without needing users to pay gas. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogChainStampedEdits { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChainStampedEdits` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChainStampedEdits.sol ChainStampedEdits` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every edit provenance action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-chain-verified-edits-9 · photography · Chain Verified Edits ===== Pitch: Verify authenticity and sequence of photo edits on-chain to certify editorial integrity. Page: https://arcprompts.lovable.app/ideas/photography-chain-verified-edits-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Chain Verified Edits" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Verify authenticity and sequence of photo edits on-chain to certify editorial integrity. Discipline: Photography (edit validation). Onchain primitive: Arc smart contract. Why this primitive: Blockchain immutably validates editing steps, preventing manipulation or forgery. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChainVerifiedEdits.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ChainVerifiedEdits /// @notice Verify authenticity and sequence of photo edits on-chain to certify editorial integrity. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ChainVerifiedEdits { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChainVerifiedEdits` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChainVerifiedEdits.sol ChainVerifiedEdits` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a edit validation action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-chain-verified-exif-19 · photography · Chain-Verified EXIF ===== Pitch: Verify and store immutable EXIF data onchain without wallet or gas barriers. Page: https://arcprompts.lovable.app/ideas/photography-chain-verified-exif-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Chain-Verified EXIF" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Verify and store immutable EXIF data onchain without wallet or gas barriers. Discipline: Photography (photo metadata). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy’s sponsored tx and wallets enable frictionless metadata proofing. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChainVerifiedEXIF.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogChainVerifiedEXIF /// @notice Verify and store immutable EXIF data onchain without wallet or gas barriers. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogChainVerifiedEXIF { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChainVerifiedEXIF` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChainVerifiedEXIF.sol ChainVerifiedEXIF` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every photo metadata action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-chaincolor-study-2 · photography · ChainColor Study ===== Pitch: Save and share color grading presets onchain without spending gas or complex wallet setup. Page: https://arcprompts.lovable.app/ideas/photography-chaincolor-study-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ChainColor Study" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Save and share color grading presets onchain without spending gas or complex wallet setup. Discipline: Photography (color grading). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored tx makes onchain preset sharing effortless. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChainColorStudy.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogChainColorStudy /// @notice Save and share color grading presets onchain without spending gas or complex wallet setup. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogChainColorStudy { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChainColorStudy` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChainColorStudy.sol ChainColorStudy` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every color grading action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-chainframe-archive-0 · photography · ChainFrame Archive ===== Pitch: Securely store and verify photo archives with immutable IPFS-backed metadata for trusted provenance. Page: https://arcprompts.lovable.app/ideas/photography-chainframe-archive-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ChainFrame Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely store and verify photo archives with immutable IPFS-backed metadata for trusted provenance. Discipline: Photography (photo archiving). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT ensures permanent, tamper-proof image storage with decentralized CID referencing. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChainFrameArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentChainFrameArchive /// @notice Securely store and verify photo archives with immutable IPFS-backed metadata for trusted provenance. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentChainFrameArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChainFrameArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChainFrameArchive.sol ChainFrameArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a photo archiving, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-chronocapture-log-11 · photography · ChronoCapture Log ===== Pitch: Pin time-lapse image sequences on IPFS to preserve and share permanent chronological photo stories. Page: https://arcprompts.lovable.app/ideas/photography-chronocapture-log-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ChronoCapture Log" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin time-lapse image sequences on IPFS to preserve and share permanent chronological photo stories. Discipline: Photography (time lapse). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinning sequences via JWT anchors time-ordered images immutably through unique CIDs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChronoCaptureLog.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentChronoCaptureLog /// @notice Pin time-lapse image sequences on IPFS to preserve and share permanent chronological photo stories. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentChronoCaptureLog { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChronoCaptureLog` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChronoCaptureLog.sol ChronoCaptureLog` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a time lapse, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-clipstatic-cdn-14 · photography · ClipStatic CDN ===== Pitch: Distribute high-performance static photo assets pinned on IPFS for reliable global delivery. Page: https://arcprompts.lovable.app/ideas/photography-clipstatic-cdn-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ClipStatic CDN" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Distribute high-performance static photo assets pinned on IPFS for reliable global delivery. Discipline: Photography (image delivery). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT ensures permanent CID anchoring for scalable decentralized hosting. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ClipStaticCDN.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentClipStaticCDN /// @notice Distribute high-performance static photo assets pinned on IPFS for reliable global delivery. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentClipStaticCDN { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ClipStaticCDN` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ClipStaticCDN.sol ClipStaticCDN` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a image delivery, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-collabochain-studio-8 · photography · CollaboChain Studio ===== Pitch: Share locked versions of photos and edits on IPFS for trustable collaborative workflows. Page: https://arcprompts.lovable.app/ideas/photography-collabochain-studio-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "CollaboChain Studio" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Share locked versions of photos and edits on IPFS for trustable collaborative workflows. Discipline: Photography (collaborative editing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinning edits and base images via JWT creates permanent, shared CID histories. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CollaboChainStudio.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentCollaboChainStudio /// @notice Share locked versions of photos and edits on IPFS for trustable collaborative workflows. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentCollaboChainStudio { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CollaboChainStudio` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CollaboChainStudio.sol CollaboChainStudio` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a collaborative editing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-collaborative-edits-chain-3 · photography · Collaborative Edits Chain ===== Pitch: Track photo edit histories and collaborators immutably on Sepolia for transparent teamwork. Page: https://arcprompts.lovable.app/ideas/photography-collaborative-edits-chain-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Collaborative Edits Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track photo edit histories and collaborators immutably on Sepolia for transparent teamwork. Discipline: Photography (collaborative editing). Onchain primitive: Arc smart contract. Why this primitive: Blockchain tracks and preserves every edit and contributor without central control. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CollaborativeEditsChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title CollaborativeEditsChain /// @notice Track photo edit histories and collaborators immutably on Sepolia for transparent teamwork. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CollaborativeEditsChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CollaborativeEditsChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CollaborativeEditsChain.sol CollaborativeEditsChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a collaborative editing action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-collaboshot-chain-7 · photography · CollaboShot Chain ===== Pitch: Securely track multi-photographer project ownership with shared NFTs. Page: https://arcprompts.lovable.app/ideas/photography-collaboshot-chain-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "CollaboShot Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely track multi-photographer project ownership with shared NFTs. Discipline: Photography (collaborative shoots). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens authenticate multi-creator ownership on immutable IPFS CIDs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CollaboShotChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title CollaboShotChain /// @notice ERC-721 provenance for: Securely track multi-photographer project ownership with shared NFTs. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CollaboShotChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("CollaboShotChain", "COLLAB") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CollaboShotChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CollaboShotChain.sol CollaboShotChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a collaborative shoots, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-colorchain-palette-2 · photography · ColorChain Palette ===== Pitch: Save and share color grading presets and images as permanent IPFS assets for consistent workflow. Page: https://arcprompts.lovable.app/ideas/photography-colorchain-palette-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ColorChain Palette" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Save and share color grading presets and images as permanent IPFS assets for consistent workflow. Discipline: Photography (color grading). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT securely stores color data and images ensuring consistent cross-device access via CID. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ColorChainPalette.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentColorChainPalette /// @notice Save and share color grading presets and images as permanent IPFS assets for consistent workflow. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentColorChainPalette { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ColorChainPalette` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ColorChainPalette.sol ColorChainPalette` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a color grading, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-colorgrade-chain-19 · photography · ColorGrade Chain ===== Pitch: Track and prove original photo color grades and presets on-chain. Page: https://arcprompts.lovable.app/ideas/photography-colorgrade-chain-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ColorGrade Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track and prove original photo color grades and presets on-chain. Discipline: Photography (color grading). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 mints unique color grade profiles linked to IPFS storage. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ColorGradeChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ColorGradeChain /// @notice ERC-721 provenance for: Track and prove original photo color grades and presets on-chain. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ColorGradeChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ColorGradeChain", "COLORG") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ColorGradeChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ColorGradeChain.sol ColorGradeChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a color grading, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-cropchain-editor-17 · photography · CropChain Editor ===== Pitch: Save crop presets and cropped images pinned on IPFS for exact reproducibility and sharing. Page: https://arcprompts.lovable.app/ideas/photography-cropchain-editor-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "CropChain Editor" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Save crop presets and cropped images pinned on IPFS for exact reproducibility and sharing. Discipline: Photography (image cropping). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT securely stores crop data alongside images with permanent CID linkage. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CropChainEditor.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentCropChainEditor /// @notice Save crop presets and cropped images pinned on IPFS for exact reproducibility and sharing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentCropChainEditor { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CropChainEditor` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CropChainEditor.sol CropChainEditor` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a image cropping, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-decentralized-event-coverage-24 · photography · Decentralized Event Coverage ===== Pitch: Coordinate and verify crowdsourced event photography via Sepolia smart contracts for reliability. Page: https://arcprompts.lovable.app/ideas/photography-decentralized-event-coverage-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Decentralized Event Coverage" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Coordinate and verify crowdsourced event photography via Sepolia smart contracts for reliability. Discipline: Photography (crowdsourced photo event). Onchain primitive: Arc smart contract. Why this primitive: Blockchain ensures verifiable contributions and transparent reward distribution. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DecentralizedEventCoverage.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DecentralizedEventCoverage /// @notice Coordinate and verify crowdsourced event photography via Sepolia smart contracts for reliability. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DecentralizedEventCoverage { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DecentralizedEventCoverage` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DecentralizedEventCoverage.sol DecentralizedEventCoverage` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a crowdsourced photo event action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-decentralized-feedback-10 · photography · Decentralized Feedback ===== Pitch: Gather verified community feedback on photos via Sepolia-based smart contract voting. Page: https://arcprompts.lovable.app/ideas/photography-decentralized-feedback-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Decentralized Feedback" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Gather verified community feedback on photos via Sepolia-based smart contract voting. Discipline: Photography (community critique). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts guarantee transparent, tamper-proof, and fair community voting on photos. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DecentralizedFeedback.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DecentralizedFeedback /// @notice Gather verified community feedback on photos via Sepolia-based smart contract voting. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DecentralizedFeedback { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DecentralizedFeedback` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DecentralizedFeedback.sol DecentralizedFeedback` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a community critique action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-decentralized-photo-auctions-17 · photography · Decentralized Photo Auctions ===== Pitch: Run transparent auctions for rare photos secured and settled using Sepolia smart contracts. Page: https://arcprompts.lovable.app/ideas/photography-decentralized-photo-auctions-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Decentralized Photo Auctions" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Run transparent auctions for rare photos secured and settled using Sepolia smart contracts. Discipline: Photography (auction platform). Onchain primitive: Arc smart contract. Why this primitive: Blockchain guarantees fair bidding and transparent settlement without intermediaries. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DecentralizedPhotoAuctions.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DecentralizedPhotoAuctions /// @notice Run transparent auctions for rare photos secured and settled using Sepolia smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DecentralizedPhotoAuctions { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DecentralizedPhotoAuctions` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DecentralizedPhotoAuctions.sol DecentralizedPhotoAuctions` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a auction platform action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-decentralized-portfolios-4 · photography · Decentralized Portfolios ===== Pitch: Create and showcase decentralized photography portfolios verified on Sepolia smart contracts. Page: https://arcprompts.lovable.app/ideas/photography-decentralized-portfolios-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Decentralized Portfolios" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create and showcase decentralized photography portfolios verified on Sepolia smart contracts. Discipline: Photography (photographer profiles). Onchain primitive: Arc smart contract. Why this primitive: Onchain portfolios guarantee authenticity and censorship resistance for photographers’ work. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DecentralizedPortfolios.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DecentralizedPortfolios /// @notice Create and showcase decentralized photography portfolios verified on Sepolia smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DecentralizedPortfolios { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DecentralizedPortfolios` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DecentralizedPortfolios.sol DecentralizedPortfolios` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a photographer profiles action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-depthmap-chain-21 · photography · DepthMap Chain ===== Pitch: Pin depth maps and photos together on IPFS to preserve immersive photo data immutably. Page: https://arcprompts.lovable.app/ideas/photography-depthmap-chain-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "DepthMap Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin depth maps and photos together on IPFS to preserve immersive photo data immutably. Discipline: Photography (3D photography). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT links image and depth layers securely with permanent CID storage. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DepthMapChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentDepthMapChain /// @notice Pin depth maps and photos together on IPFS to preserve immersive photo data immutably. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentDepthMapChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DepthMapChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DepthMapChain.sol DepthMapChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a 3D photography, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-droneshot-ledger-14 · photography · DroneShot Ledger ===== Pitch: Track drone photo creations with immutable blockchain provenance. Page: https://arcprompts.lovable.app/ideas/photography-droneshot-ledger-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "DroneShot Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track drone photo creations with immutable blockchain provenance. Discipline: Photography (drone photography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT minting provides secure, traceable drone imagery ownership proof. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DroneShotLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title DroneShotLedger /// @notice ERC-721 provenance for: Track drone photo creations with immutable blockchain provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DroneShotLedger is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("DroneShotLedger", "DRONES") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DroneShotLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DroneShotLedger.sol DroneShotLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a drone photography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-edittrace-chain-1 · photography · EditTrace Chain ===== Pitch: Track complete edit histories on-chain for transparent creative workflows. Page: https://arcprompts.lovable.app/ideas/photography-edittrace-chain-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "EditTrace Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track complete edit histories on-chain for transparent creative workflows. Discipline: Photography (photo editing history). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens record each edit step linked to immutable IPFS hashes. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EditTraceChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title EditTraceChain /// @notice ERC-721 provenance for: Track complete edit histories on-chain for transparent creative workflows. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract EditTraceChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("EditTraceChain", "EDITTR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EditTraceChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EditTraceChain.sol EditTraceChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a photo editing history, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-eventsnap-token-18 · photography · EventSnap Token ===== Pitch: Guarantee wedding photo originality and ownership with NFT provenance. Page: https://arcprompts.lovable.app/ideas/photography-eventsnap-token-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "EventSnap Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Guarantee wedding photo originality and ownership with NFT provenance. Discipline: Photography (wedding photography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT minting records each wedding photo’s creator and IPFS content hash. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EventSnapToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title EventSnapToken /// @notice ERC-721 provenance for: Guarantee wedding photo originality and ownership with NFT provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract EventSnapToken is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("EventSnapToken", "EVENTS") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EventSnapToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EventSnapToken.sol EventSnapToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a wedding photography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-exposuretrace-6 · photography · ExposureTrace ===== Pitch: Store exposure settings and images together on IPFS to improve reproducibility and learning. Page: https://arcprompts.lovable.app/ideas/photography-exposuretrace-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ExposureTrace" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store exposure settings and images together on IPFS to improve reproducibility and learning. Discipline: Photography (exposure analysis). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinning exposure data with PinataJWT creates unalterable records linked to each image CID. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ExposureTrace.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentExposureTrace /// @notice Store exposure settings and images together on IPFS to improve reproducibility and learning. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentExposureTrace { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ExposureTrace` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ExposureTrace.sol ExposureTrace` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a exposure analysis, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-filterchain-exchange-19 · photography · FilterChain Exchange ===== Pitch: Publish photography filters and their sample images pinned on IPFS for permanent decentralized sharing. Page: https://arcprompts.lovable.app/ideas/photography-filterchain-exchange-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FilterChain Exchange" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Publish photography filters and their sample images pinned on IPFS for permanent decentralized sharing. Discipline: Photography (filter sharing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinning filters and examples with Pinata JWT ensures immutable accessible filter assets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FilterChainExchange.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentFilterChainExchange /// @notice Publish photography filters and their sample images pinned on IPFS for permanent decentralized sharing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentFilterChainExchange { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FilterChainExchange` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FilterChainExchange.sol FilterChainExchange` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a filter sharing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-filtermint-chain-15 · photography · FilterMint Chain ===== Pitch: Prove filter creation and usage history on-chain for unique photo effects. Page: https://arcprompts.lovable.app/ideas/photography-filtermint-chain-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FilterMint Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Prove filter creation and usage history on-chain for unique photo effects. Discipline: Photography (creative filters). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 records filter ownership linked to IPFS-hosted effect metadata. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FilterMintChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title FilterMintChain /// @notice ERC-721 provenance for: Prove filter creation and usage history on-chain for unique photo effects. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FilterMintChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("FilterMintChain", "FILTER") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FilterMintChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FilterMintChain.sol FilterMintChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a creative filters, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-focusproof-network-5 · photography · FocusProof Network ===== Pitch: Record lens focus data alongside images on IPFS for verifiable sharpness and authenticity. Page: https://arcprompts.lovable.app/ideas/photography-focusproof-network-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FocusProof Network" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record lens focus data alongside images on IPFS for verifiable sharpness and authenticity. Discipline: Photography (focus tracking). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT uploads link images with focus metadata immutably on IPFS via permanent CID. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FocusProofNetwork.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentFocusProofNetwork /// @notice Record lens focus data alongside images on IPFS for verifiable sharpness and authenticity. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentFocusProofNetwork { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FocusProofNetwork` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FocusProofNetwork.sol FocusProofNetwork` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a focus tracking, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-focusproof-vault-10 · photography · FocusProof Vault ===== Pitch: Use on-chain minted NFTs as legally admissible photo evidence. Page: https://arcprompts.lovable.app/ideas/photography-focusproof-vault-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FocusProof Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Use on-chain minted NFTs as legally admissible photo evidence. Discipline: Photography (photo legal evidence). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Immutable NFT ownership and IPFS content hashes validate photo authenticity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FocusProofVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title FocusProofVault /// @notice ERC-721 provenance for: Use on-chain minted NFTs as legally admissible photo evidence. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FocusProofVault is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("FocusProofVault", "FOCUSP") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FocusProofVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FocusProofVault.sol FocusProofVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a photo legal evidence, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-framechain-gallery-9 · photography · FrameChain Gallery ===== Pitch: Create blockchain-backed digital photo galleries with verified provenance. Page: https://arcprompts.lovable.app/ideas/photography-framechain-gallery-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FrameChain Gallery" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create blockchain-backed digital photo galleries with verified provenance. Discipline: Photography (digital galleries). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens mint and prove ownership of gallery collections on IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FrameChainGallery.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title FrameChainGallery /// @notice ERC-721 provenance for: Create blockchain-backed digital photo galleries with verified provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FrameChainGallery is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("FrameChainGallery", "FRAMEC") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FrameChainGallery` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FrameChainGallery.sol FrameChainGallery` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a digital galleries, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-framesplit-archive-22 · photography · FrameSplit Archive ===== Pitch: Pin segmented photo layers on IPFS for permanent storage and collaborative recombination. Page: https://arcprompts.lovable.app/ideas/photography-framesplit-archive-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FrameSplit Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin segmented photo layers on IPFS for permanent storage and collaborative recombination. Discipline: Photography (photo segmentation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT uploads ensure immutable access to each segmented layer via unique CID. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FrameSplitArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentFrameSplitArchive /// @notice Pin segmented photo layers on IPFS for permanent storage and collaborative recombination. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentFrameSplitArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FrameSplitArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FrameSplitArchive.sol FrameSplitArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a photo segmentation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-frameswap-marketplace-13 · photography · FrameSwap Marketplace ===== Pitch: Trade and pin licensed photo assets on IPFS to enable decentralized, tamper-proof exchanges. Page: https://arcprompts.lovable.app/ideas/photography-frameswap-marketplace-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FrameSwap Marketplace" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Trade and pin licensed photo assets on IPFS to enable decentralized, tamper-proof exchanges. Discipline: Photography (photo asset exchange). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT guarantees permanent storage and proof of ownership via IPFS CID. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FrameSwapMarketplace.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentFrameSwapMarketplace /// @notice Trade and pin licensed photo assets on IPFS to enable decentralized, tamper-proof exchanges. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentFrameSwapMarketplace { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FrameSwapMarketplace` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FrameSwapMarketplace.sol FrameSwapMarketplace` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a photo asset exchange, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-gas-free-collaboration-9 · photography · Gas-Free Collaboration ===== Pitch: Collaborate on photo edits and shares onchain with zero gas fees for team members. Page: https://arcprompts.lovable.app/ideas/photography-gas-free-collaboration-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gas-Free Collaboration" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Collaborate on photo edits and shares onchain with zero gas fees for team members. Discipline: Photography (team workflow). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored transactions enables seamless multi-user workflows. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GasFreeCollaboration.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGasFreeCollaboration /// @notice Collaborate on photo edits and shares onchain with zero gas fees for team members. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGasFreeCollaboration { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GasFreeCollaboration` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GasFreeCollaboration.sol GasFreeCollaboration` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every team workflow action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-gas-free-prints-order-3 · photography · Gas-Free Prints Order ===== Pitch: Order professional photo prints onchain with gasless transactions for instant confirmation. Page: https://arcprompts.lovable.app/ideas/photography-gas-free-prints-order-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gas-Free Prints Order" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Order professional photo prints onchain with gasless transactions for instant confirmation. Discipline: Photography (print ordering). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored transactions ensure print orders occur smoothly without user gas payments. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GasFreePrintsOrder.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGasFreePrintsOrder /// @notice Order professional photo prints onchain with gasless transactions for instant confirmation. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGasFreePrintsOrder { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GasFreePrintsOrder` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GasFreePrintsOrder.sol GasFreePrintsOrder` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every print ordering action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-gasless-gallery-0 · photography · Gasless Gallery ===== Pitch: Seamlessly share photos onchain with no gas fees for your audience and collaborators. Page: https://arcprompts.lovable.app/ideas/photography-gasless-gallery-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Gallery" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Seamlessly share photos onchain with no gas fees for your audience and collaborators. Discipline: Photography (photo sharing). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored tx remove gas friction, enabling smooth social sharing with Privy wallets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessGallery.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessGallery /// @notice Seamlessly share photos onchain with no gas fees for your audience and collaborators. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessGallery { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessGallery` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessGallery.sol GaslessGallery` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every photo sharing action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-gasless-metadata-tags-6 · photography · Gasless Metadata Tags ===== Pitch: Add and sync photo metadata onchain without gas fees or wallet complications. Page: https://arcprompts.lovable.app/ideas/photography-gasless-metadata-tags-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Metadata Tags" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Add and sync photo metadata onchain without gas fees or wallet complications. Discipline: Photography (photo metadata). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored tx enables seamless metadata updates for photographers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessMetadataTags.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessMetadataTags /// @notice Add and sync photo metadata onchain without gas fees or wallet complications. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessMetadataTags { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessMetadataTags` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessMetadataTags.sol GaslessMetadataTags` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every photo metadata action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-gasless-photo-auctions-12 · photography · Gasless Photo Auctions ===== Pitch: Host and join photo auctions onchain with zero gas fees and simple wallet integration. Page: https://arcprompts.lovable.app/ideas/photography-gasless-photo-auctions-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Photo Auctions" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Host and join photo auctions onchain with zero gas fees and simple wallet integration. Discipline: Photography (photo auctions). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored tx plus Privy wallets reduce friction in auction bids and sales. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessPhotoAuctions.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessPhotoAuctions /// @notice Host and join photo auctions onchain with zero gas fees and simple wallet integration. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessPhotoAuctions { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessPhotoAuctions` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessPhotoAuctions.sol GaslessPhotoAuctions` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every photo auctions action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-gasless-photo-badges-20 · photography · Gasless Photo Badges ===== Pitch: Earn and showcase onchain badges for photography achievements with zero gas fees. Page: https://arcprompts.lovable.app/ideas/photography-gasless-photo-badges-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Photo Badges" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Earn and showcase onchain badges for photography achievements with zero gas fees. Discipline: Photography (community rewards). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored tx allows effortless badge minting and display. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessPhotoBadges.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessPhotoBadges /// @notice Earn and showcase onchain badges for photography achievements with zero gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessPhotoBadges { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessPhotoBadges` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessPhotoBadges.sol GaslessPhotoBadges` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every community rewards action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-gasless-watermark-tags-16 · photography · Gasless Watermark Tags ===== Pitch: Apply and verify photo watermarks onchain without wallet or gas hassles. Page: https://arcprompts.lovable.app/ideas/photography-gasless-watermark-tags-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Watermark Tags" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Apply and verify photo watermarks onchain without wallet or gas hassles. Discipline: Photography (copyright protection). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social and sponsored tx ensure smooth rights protection tagging. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessWatermarkTags.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessWatermarkTags /// @notice Apply and verify photo watermarks onchain without wallet or gas hassles. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessWatermarkTags { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessWatermarkTags` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessWatermarkTags.sol GaslessWatermarkTags` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every copyright protection action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-hdr-pinboard-16 · photography · HDR Pinboard ===== Pitch: Pin multi-exposure HDR image sets on IPFS to ensure permanent, verifiable combination assets. Page: https://arcprompts.lovable.app/ideas/photography-hdr-pinboard-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "HDR Pinboard" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin multi-exposure HDR image sets on IPFS to ensure permanent, verifiable combination assets. Discipline: Photography (high dynamic range). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinning HDR frames via JWT creates immutable grouped CID references for complete sets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/HDRPinboard.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentHDRPinboard /// @notice Pin multi-exposure HDR image sets on IPFS to ensure permanent, verifiable combination assets. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentHDRPinboard { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs HDRPinboard` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/HDRPinboard.sol HDRPinboard` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a high dynamic range, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-immutable-photo-rights-0 · photography · Immutable Photo Rights ===== Pitch: Securely register photo ownership and licensing rights on-chain with immutable proof. Page: https://arcprompts.lovable.app/ideas/photography-immutable-photo-rights-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Immutable Photo Rights" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely register photo ownership and licensing rights on-chain with immutable proof. Discipline: Photography (copyright registry). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts ensure tamper-proof, transparent ownership records accessible worldwide. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ImmutablePhotoRights.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ImmutablePhotoRights /// @notice Securely register photo ownership and licensing rights on-chain with immutable proof. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ImmutablePhotoRights { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ImmutablePhotoRights` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ImmutablePhotoRights.sol ImmutablePhotoRights` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a copyright registry action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-instant-rights-ledger-5 · photography · Instant Rights Ledger ===== Pitch: Manage photo licensing rights transparently onchain with no gas fees for creators. Page: https://arcprompts.lovable.app/ideas/photography-instant-rights-ledger-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Instant Rights Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Manage photo licensing rights transparently onchain with no gas fees for creators. Discipline: Photography (rights management). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallets and sponsored tx ensure smooth rights record without wallet barriers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InstantRightsLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogInstantRightsLedger /// @notice Manage photo licensing rights transparently onchain with no gas fees for creators. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogInstantRightsLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InstantRightsLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InstantRightsLedger.sol InstantRightsLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every rights management action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-lenslegacy-vault-12 · photography · LensLegacy Vault ===== Pitch: Store detailed camera sensor data with images on IPFS to analyze vintage gear performance over time. Page: https://arcprompts.lovable.app/ideas/photography-lenslegacy-vault-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LensLegacy Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store detailed camera sensor data with images on IPFS to analyze vintage gear performance over time. Discipline: Photography (camera data archival). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT pins sensor metadata and images immutably with verifiable CID links. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LensLegacyVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentLensLegacyVault /// @notice Store detailed camera sensor data with images on IPFS to analyze vintage gear performance over time. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentLensLegacyVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LensLegacyVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LensLegacyVault.sol LensLegacyVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a camera data archival, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-lensstory-archive-6 · photography · LensStory Archive ===== Pitch: Create permanent, traceable archives of photo collections on blockchain. Page: https://arcprompts.lovable.app/ideas/photography-lensstory-archive-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LensStory Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create permanent, traceable archives of photo collections on blockchain. Discipline: Photography (photography archives). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 minting records collection provenance linked to IPFS metadata. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LensStoryArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title LensStoryArchive /// @notice ERC-721 provenance for: Create permanent, traceable archives of photo collections on blockchain. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract LensStoryArchive is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("LensStoryArchive", "LENSST") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LensStoryArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LensStoryArchive.sol LensStoryArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a photography archives, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-macrofocus-token-21 · photography · MacroFocus Token ===== Pitch: Securely mint and verify product photo assets via on-chain provenance. Page: https://arcprompts.lovable.app/ideas/photography-macrofocus-token-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MacroFocus Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely mint and verify product photo assets via on-chain provenance. Discipline: Photography (product photography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs linked to IPFS ensure trusted ownership and royalty tracking. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MacroFocusToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MacroFocusToken /// @notice ERC-721 provenance for: Securely mint and verify product photo assets via on-chain provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MacroFocusToken is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MacroFocusToken", "MACROF") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MacroFocusToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MacroFocusToken.sol MacroFocusToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a product photography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-macromint-origins-11 · photography · MacroMint Origins ===== Pitch: Authenticate original macro shots with creator-owned NFTs. Page: https://arcprompts.lovable.app/ideas/photography-macromint-origins-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MacroMint Origins" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate original macro shots with creator-owned NFTs. Discipline: Photography (macro photography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 ensures unique, tamper-proof records linked to IPFS macro images. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MacroMintOrigins.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MacroMintOrigins /// @notice ERC-721 provenance for: Authenticate original macro shots with creator-owned NFTs. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MacroMintOrigins is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MacroMintOrigins", "MACROM") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MacroMintOrigins` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MacroMintOrigins.sol MacroMintOrigins` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a macro photography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-metaframe-vault-3 · photography · MetaFrame Vault ===== Pitch: Embed and immutably store photo metadata on IPFS to preserve creator rights and data integrity. Page: https://arcprompts.lovable.app/ideas/photography-metaframe-vault-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MetaFrame Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Embed and immutably store photo metadata on IPFS to preserve creator rights and data integrity. Discipline: Photography (metadata embedding). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS pinning via JWT guarantees permanent metadata storage linked to each image CID. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MetaFrameVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMetaFrameVault /// @notice Embed and immutably store photo metadata on IPFS to preserve creator rights and data integrity. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMetaFrameVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MetaFrameVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MetaFrameVault.sol MetaFrameVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a metadata embedding, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-mirrorframe-backup-24 · photography · MirrorFrame Backup ===== Pitch: Create decentralized backups of photos pinned on IPFS to protect against data loss permanently. Page: https://arcprompts.lovable.app/ideas/photography-mirrorframe-backup-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MirrorFrame Backup" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create decentralized backups of photos pinned on IPFS to protect against data loss permanently. Discipline: Photography (image backup). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT offers permanent, redundant IPFS storage for safe off-site photo backup. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MirrorFrameBackup.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMirrorFrameBackup /// @notice Create decentralized backups of photos pinned on IPFS to protect against data loss permanently. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMirrorFrameBackup { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MirrorFrameBackup` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MirrorFrameBackup.sol MirrorFrameBackup` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a image backup, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-momentmap-7 · photography · MomentMap ===== Pitch: Pin geotagged photos on IPFS to create a permanent, verifiable map of photographic moments. Page: https://arcprompts.lovable.app/ideas/photography-momentmap-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MomentMap" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin geotagged photos on IPFS to create a permanent, verifiable map of photographic moments. Discipline: Photography (location tagging). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT ensures location data and images remain bound immutably via IPFS CID. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MomentMap.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMomentMap /// @notice Pin geotagged photos on IPFS to create a permanent, verifiable map of photographic moments. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMomentMap { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MomentMap` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MomentMap.sol MomentMap` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a location tagging, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-newsframe-provenance-4 · photography · NewsFrame Provenance ===== Pitch: Authenticate frontline news photos with immutable creator-owned tokens. Page: https://arcprompts.lovable.app/ideas/photography-newsframe-provenance-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NewsFrame Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate frontline news photos with immutable creator-owned tokens. Discipline: Photography (photojournalism). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 on Sepolia guarantees verified news photo provenance and ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NewsFrameProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title NewsFrameProvenance /// @notice ERC-721 provenance for: Authenticate frontline news photos with immutable creator-owned tokens. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract NewsFrameProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("NewsFrameProvenance", "NEWSFR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NewsFrameProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NewsFrameProvenance.sol NewsFrameProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a photojournalism, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-nft-photo-stories-1 · photography · NFT Photo Stories ===== Pitch: Create and share onchain photo stories with built-in provenance and no wallet setup hassle. Page: https://arcprompts.lovable.app/ideas/photography-nft-photo-stories-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NFT Photo Stories" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create and share onchain photo stories with built-in provenance and no wallet setup hassle. Discipline: Photography (photojournalism). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets plus sponsored tx enable easy entry for photojournalists. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NFTPhotoStories.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogNFTPhotoStories /// @notice Create and share onchain photo stories with built-in provenance and no wallet setup hassle. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogNFTPhotoStories { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NFTPhotoStories` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NFTPhotoStories.sol NFTPhotoStories` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every photojournalism action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-nft-prints-marketplace-6 · photography · NFT Prints Marketplace ===== Pitch: Sell limited edition photo prints linked to NFTs minted and verified on Sepolia. Page: https://arcprompts.lovable.app/ideas/photography-nft-prints-marketplace-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NFT Prints Marketplace" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Sell limited edition photo prints linked to NFTs minted and verified on Sepolia. Discipline: Photography (photo prints trading). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts ensure scarcity and authenticity of photo print NFTs for collectors. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NFTPrintsMarketplace.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title NFTPrintsMarketplace /// @notice Sell limited edition photo prints linked to NFTs minted and verified on Sepolia. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract NFTPrintsMarketplace { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NFTPrintsMarketplace` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NFTPrintsMarketplace.sol NFTPrintsMarketplace` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a photo prints trading action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-nftrography-gallery-9 · photography · NFTrography Gallery ===== Pitch: Mint and pin unique photographic art on IPFS to guarantee immutable ownership and provenance. Page: https://arcprompts.lovable.app/ideas/photography-nftrography-gallery-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NFTrography Gallery" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint and pin unique photographic art on IPFS to guarantee immutable ownership and provenance. Discipline: Photography (photo NFTs). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT uploads ensure permanent IPFS storage of NFT images and metadata. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NFTrographyGallery.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentNFTrographyGallery /// @notice Mint and pin unique photographic art on IPFS to guarantee immutable ownership and provenance. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentNFTrographyGallery { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NFTrographyGallery` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NFTrographyGallery.sol NFTrographyGallery` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a photo NFTs, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-nightshot-mint-24 · photography · NightShot Mint ===== Pitch: Secure and verify ownership of night photos with NFT provenance. Page: https://arcprompts.lovable.app/ideas/photography-nightshot-mint-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NightShot Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Secure and verify ownership of night photos with NFT provenance. Discipline: Photography (night photography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens create immutable proofs tied to IPFS night photo data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NightShotMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title NightShotMint /// @notice ERC-721 provenance for: Secure and verify ownership of night photos with NFT provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract NightShotMint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("NightShotMint", "NIGHTS") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NightShotMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NightShotMint.sol NightShotMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a night photography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-onchain-event-albums-10 · photography · Onchain Event Albums ===== Pitch: Create and distribute event photo albums onchain with no gas fees for attendees. Page: https://arcprompts.lovable.app/ideas/photography-onchain-event-albums-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Event Albums" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create and distribute event photo albums onchain with no gas fees for attendees. Discipline: Photography (event photography). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored tx and Privy wallets enable effortless event photo sharing at scale. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainEventAlbums.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogOnchainEventAlbums /// @notice Create and distribute event photo albums onchain with no gas fees for attendees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogOnchainEventAlbums { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainEventAlbums` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainEventAlbums.sol OnchainEventAlbums` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every event photography action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-onchain-lens-reviews-21 · photography · Onchain Lens Reviews ===== Pitch: Submit and share authentic lens reviews onchain with no gas required from users. Page: https://arcprompts.lovable.app/ideas/photography-onchain-lens-reviews-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Lens Reviews" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Submit and share authentic lens reviews onchain with no gas required from users. Discipline: Photography (equipment reviews). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored tx with Privy wallets guarantees easy user contributions. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainLensReviews.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogOnchainLensReviews /// @notice Submit and share authentic lens reviews onchain with no gas required from users. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogOnchainLensReviews { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainLensReviews` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainLensReviews.sol OnchainLensReviews` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every equipment reviews action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-onchain-model-releases-20 · photography · Onchain Model Releases ===== Pitch: Digitally sign and store model release forms on Sepolia blockchain for immutable proof. Page: https://arcprompts.lovable.app/ideas/photography-onchain-model-releases-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Model Releases" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Digitally sign and store model release forms on Sepolia blockchain for immutable proof. Discipline: Photography (legal documentation). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts provide tamper-proof, verifiable legal agreements. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainModelReleases.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainModelReleases /// @notice Digitally sign and store model release forms on Sepolia blockchain for immutable proof. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainModelReleases { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainModelReleases` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainModelReleases.sol OnchainModelReleases` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a legal documentation action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-onchain-photo-challenges-12 · photography · Onchain Photo Challenges ===== Pitch: Host transparent and decentralized photo contests with onchain submission and voting. Page: https://arcprompts.lovable.app/ideas/photography-onchain-photo-challenges-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Photo Challenges" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Host transparent and decentralized photo contests with onchain submission and voting. Discipline: Photography (contest management). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts ensure fairness and transparency of submissions and awards. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainPhotoChallenges.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainPhotoChallenges /// @notice Host transparent and decentralized photo contests with onchain submission and voting. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainPhotoChallenges { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainPhotoChallenges` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainPhotoChallenges.sol OnchainPhotoChallenges` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a contest management action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-onchain-photo-licensing-15 · photography · Onchain Photo Licensing ===== Pitch: Negotiate and finalize photo licenses onchain with no gas fees for quick deals. Page: https://arcprompts.lovable.app/ideas/photography-onchain-photo-licensing-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Photo Licensing" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Negotiate and finalize photo licenses onchain with no gas fees for quick deals. Discipline: Photography (rights clearing). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy sponsored tx removes friction for fast onchain license agreements. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainPhotoLicensing.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogOnchainPhotoLicensing /// @notice Negotiate and finalize photo licenses onchain with no gas fees for quick deals. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogOnchainPhotoLicensing { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainPhotoLicensing` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainPhotoLicensing.sol OnchainPhotoLicensing` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every rights clearing action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-onchain-photo-licensing-16 · photography · Onchain Photo Licensing ===== Pitch: Create customizable photo license agreements executed automatically via Sepolia contracts. Page: https://arcprompts.lovable.app/ideas/photography-onchain-photo-licensing-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Photo Licensing" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create customizable photo license agreements executed automatically via Sepolia contracts. Discipline: Photography (license contracts). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts automate enforcement and execution of license terms trustlessly. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainPhotoLicensing.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainPhotoLicensing /// @notice Create customizable photo license agreements executed automatically via Sepolia contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainPhotoLicensing { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainPhotoLicensing` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainPhotoLicensing.sol OnchainPhotoLicensing` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a license contracts action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-onchain-photo-presets-23 · photography · Onchain Photo Presets ===== Pitch: Create, license, and verify photo editing presets with immutable smart contract records. Page: https://arcprompts.lovable.app/ideas/photography-onchain-photo-presets-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Photo Presets" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create, license, and verify photo editing presets with immutable smart contract records. Discipline: Photography (preset licensing). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts prove preset originality and license transfers transparently. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainPhotoPresets.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainPhotoPresets /// @notice Create, license, and verify photo editing presets with immutable smart contract records. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainPhotoPresets { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainPhotoPresets` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainPhotoPresets.sol OnchainPhotoPresets` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a preset licensing action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-onchain-photo-proof-1 · photography · Onchain Photo Proof ===== Pitch: Embed verifiable metadata on Sepolia to prove photo authenticity and timestamp creation. Page: https://arcprompts.lovable.app/ideas/photography-onchain-photo-proof-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Photo Proof" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Embed verifiable metadata on Sepolia to prove photo authenticity and timestamp creation. Discipline: Photography (image authenticity). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts provide trustless, verifiable timestamping and metadata anchoring. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainPhotoProof.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainPhotoProof /// @notice Embed verifiable metadata on Sepolia to prove photo authenticity and timestamp creation. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainPhotoProof { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainPhotoProof` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainPhotoProof.sol OnchainPhotoProof` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a image authenticity action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-onchain-usage-tracking-5 · photography · Onchain Usage Tracking ===== Pitch: Track and record photo usage and distribution on-chain in real time for accurate licensing. Page: https://arcprompts.lovable.app/ideas/photography-onchain-usage-tracking-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Onchain Usage Tracking" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track and record photo usage and distribution on-chain in real time for accurate licensing. Discipline: Photography (photo usage monitoring). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts enable decentralized, verifiable tracking immutable by any party. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OnchainUsageTracking.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title OnchainUsageTracking /// @notice Track and record photo usage and distribution on-chain in real time for accurate licensing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OnchainUsageTracking { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OnchainUsageTracking` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OnchainUsageTracking.sol OnchainUsageTracking` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a photo usage monitoring action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-photo-data-monetization-21 · photography · Photo Data Monetization ===== Pitch: Monetize photo metadata and usage data transparently via Sepolia smart contracts. Page: https://arcprompts.lovable.app/ideas/photography-photo-data-monetization-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Photo Data Monetization" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Monetize photo metadata and usage data transparently via Sepolia smart contracts. Discipline: Photography (data marketplace). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts enable secure, permissioned data sales without intermediaries. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PhotoDataMonetization.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PhotoDataMonetization /// @notice Monetize photo metadata and usage data transparently via Sepolia smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PhotoDataMonetization { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PhotoDataMonetization` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PhotoDataMonetization.sol PhotoDataMonetization` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a data marketplace action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-photoset-chain-22 · photography · PhotoSet Chain ===== Pitch: Prove ownership and authenticity of curated photo series collections. Page: https://arcprompts.lovable.app/ideas/photography-photoset-chain-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PhotoSet Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Prove ownership and authenticity of curated photo series collections. Discipline: Photography (photo series). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens mint entire series linked immutably to IPFS galleries. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PhotoSetChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PhotoSetChain /// @notice ERC-721 provenance for: Prove ownership and authenticity of curated photo series collections. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PhotoSetChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PhotoSetChain", "PHOTOS") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PhotoSetChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PhotoSetChain.sol PhotoSetChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a photo series, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-photostory-token-16 · photography · PhotoStory Token ===== Pitch: Attach narrative stories securely to photos using NFT provenance. Page: https://arcprompts.lovable.app/ideas/photography-photostory-token-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PhotoStory Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Attach narrative stories securely to photos using NFT provenance. Discipline: Photography (photo narratives). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Minting NFTs with IPFS metadata links photos to verified creator stories. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PhotoStoryToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PhotoStoryToken /// @notice ERC-721 provenance for: Attach narrative stories securely to photos using NFT provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PhotoStoryToken is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PhotoStoryToken", "PHOTOS") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PhotoStoryToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PhotoStoryToken.sol PhotoStoryToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a photo narratives, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-pinphoto-journal-1 · photography · PinPhoto Journal ===== Pitch: Verify and timestamp news photos on IPFS to ensure authentic, unaltered media for journalism. Page: https://arcprompts.lovable.app/ideas/photography-pinphoto-journal-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PinPhoto Journal" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Verify and timestamp news photos on IPFS to ensure authentic, unaltered media for journalism. Discipline: Photography (photojournalism documentation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinning images with JWT certifies and preserves photojournalistic integrity via immutable CID links. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PinPhotoJournal.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPinPhotoJournal /// @notice Verify and timestamp news photos on IPFS to ensure authentic, unaltered media for journalism. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPinPhotoJournal { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PinPhotoJournal` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PinPhotoJournal.sol PinPhotoJournal` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a photojournalism documentation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-pixelproof-sharing-18 · photography · PixelProof Sharing ===== Pitch: Pin watermarked photos and proofs on IPFS to prevent unauthorized reuse with traceable CID. Page: https://arcprompts.lovable.app/ideas/photography-pixelproof-sharing-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PixelProof Sharing" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin watermarked photos and proofs on IPFS to prevent unauthorized reuse with traceable CID. Discipline: Photography (image watermarking). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT embeds watermarks with IPFS pins guaranteeing permanent, referenceable images. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PixelProofSharing.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPixelProofSharing /// @notice Pin watermarked photos and proofs on IPFS to prevent unauthorized reuse with traceable CID. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPixelProofSharing { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PixelProofSharing` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PixelProofSharing.sol PixelProofSharing` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a image watermarking, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-portraitclaim-chain-13 · photography · PortraitClaim Chain ===== Pitch: Secure portrait ownership and licensing with NFT provenance minting. Page: https://arcprompts.lovable.app/ideas/photography-portraitclaim-chain-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PortraitClaim Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Secure portrait ownership and licensing with NFT provenance minting. Discipline: Photography (portrait photography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens validate individual portrait copyright tied to IPFS images. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PortraitClaimChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PortraitClaimChain /// @notice ERC-721 provenance for: Secure portrait ownership and licensing with NFT provenance minting. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PortraitClaimChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PortraitClaimChain", "PORTRA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PortraitClaimChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PortraitClaimChain.sol PortraitClaimChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a portrait photography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-privy-nft-experiments-8 · photography · Privy NFT Experiments ===== Pitch: Easily mint and share photo NFTs with zero-gas user experience via Privy wallets. Page: https://arcprompts.lovable.app/ideas/photography-privy-nft-experiments-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Privy NFT Experiments" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Easily mint and share photo NFTs with zero-gas user experience via Privy wallets. Discipline: Photography (photo NFT art). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallets plus sponsor tx create frictionless NFT minting for photographers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PrivyNFTExperiments.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPrivyNFTExperiments /// @notice Easily mint and share photo NFTs with zero-gas user experience via Privy wallets. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPrivyNFTExperiments { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PrivyNFTExperiments` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PrivyNFTExperiments.sol PrivyNFTExperiments` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every photo NFT art action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-privy-photo-assets-17 · photography · Privy Photo Assets ===== Pitch: Manage photo assets onchain with no gas fees for easy organization and sharing. Page: https://arcprompts.lovable.app/ideas/photography-privy-photo-assets-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Privy Photo Assets" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Manage photo assets onchain with no gas fees for easy organization and sharing. Discipline: Photography (asset management). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet plus sponsored tx simplify asset governance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PrivyPhotoAssets.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPrivyPhotoAssets /// @notice Manage photo assets onchain with no gas fees for easy organization and sharing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPrivyPhotoAssets { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PrivyPhotoAssets` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PrivyPhotoAssets.sol PrivyPhotoAssets` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every asset management action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-privy-photo-bounties-4 · photography · Privy Photo Bounties ===== Pitch: Create gasless, onchain photo curation challenges with built-in rewards and easy entry. Page: https://arcprompts.lovable.app/ideas/photography-privy-photo-bounties-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Privy Photo Bounties" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create gasless, onchain photo curation challenges with built-in rewards and easy entry. Discipline: Photography (community curation). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets support sponsored tx for frictionless community-driven contests. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PrivyPhotoBounties.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPrivyPhotoBounties /// @notice Create gasless, onchain photo curation challenges with built-in rewards and easy entry. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPrivyPhotoBounties { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PrivyPhotoBounties` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PrivyPhotoBounties.sol PrivyPhotoBounties` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every community curation action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-privy-photo-feedback-11 · photography · Privy Photo Feedback ===== Pitch: Get instant, onchain photo feedback from peers with sponsored no-gas transactions. Page: https://arcprompts.lovable.app/ideas/photography-privy-photo-feedback-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Privy Photo Feedback" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Get instant, onchain photo feedback from peers with sponsored no-gas transactions. Discipline: Photography (photo critique). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallets plus gasless tx unlock easy social critique. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PrivyPhotoFeedback.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPrivyPhotoFeedback /// @notice Get instant, onchain photo feedback from peers with sponsored no-gas transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPrivyPhotoFeedback { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PrivyPhotoFeedback` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PrivyPhotoFeedback.sol PrivyPhotoFeedback` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every photo critique action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-privy-photo-grants-22 · photography · Privy Photo Grants ===== Pitch: Distribute photography project grants onchain with sponsored transactions removing gas costs. Page: https://arcprompts.lovable.app/ideas/photography-privy-photo-grants-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Privy Photo Grants" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Distribute photography project grants onchain with sponsored transactions removing gas costs. Discipline: Photography (creative funding). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets plus sponsored tx enable seamless fund transfer to creatives. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PrivyPhotoGrants.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPrivyPhotoGrants /// @notice Distribute photography project grants onchain with sponsored transactions removing gas costs. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPrivyPhotoGrants { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PrivyPhotoGrants` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PrivyPhotoGrants.sol PrivyPhotoGrants` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every creative funding action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-proof-of-capture-7 · photography · Proof of Capture ===== Pitch: Validate photo capture location and time on-chain instantly using Sepolia smart contracts. Page: https://arcprompts.lovable.app/ideas/photography-proof-of-capture-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Proof of Capture" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Validate photo capture location and time on-chain instantly using Sepolia smart contracts. Discipline: Photography (capture verification). Onchain primitive: Arc smart contract. Why this primitive: Immutable timestamp and geodata registration ensures real-time proof of photo origins. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ProofofCapture.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ProofofCapture /// @notice Validate photo capture location and time on-chain instantly using Sepolia smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ProofofCapture { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ProofofCapture` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ProofofCapture.sol ProofofCapture` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a capture verification action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-proofshot-certify-10 · photography · ProofShot Certify ===== Pitch: Authenticate original photos by pinning hashes and images on IPFS for proof of authenticity. Page: https://arcprompts.lovable.app/ideas/photography-proofshot-certify-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ProofShot Certify" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate original photos by pinning hashes and images on IPFS for proof of authenticity. Discipline: Photography (image authentication). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT enables immutable CID-based proof linking photo and hash data securely. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ProofShotCertify.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentProofShotCertify /// @notice Authenticate original photos by pinning hashes and images on IPFS for proof of authenticity. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentProofShotCertify { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ProofShotCertify` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ProofShotCertify.sol ProofShotCertify` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a image authentication, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-proprint-certify-2 · photography · ProPrint Certify ===== Pitch: Certify fine art photo prints with tamper-proof digital provenance. Page: https://arcprompts.lovable.app/ideas/photography-proprint-certify-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ProPrint Certify" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Certify fine art photo prints with tamper-proof digital provenance. Discipline: Photography (fine art prints). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT minting on Sepolia links digital print files and ownership records securely. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ProPrintCertify.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ProPrintCertify /// @notice ERC-721 provenance for: Certify fine art photo prints with tamper-proof digital provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ProPrintCertify is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ProPrintCertify", "PROPRI") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ProPrintCertify` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ProPrintCertify.sol ProPrintCertify` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a fine art prints, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-smart-photo-royalties-2 · photography · Smart Photo Royalties ===== Pitch: Automate and transparently distribute photo royalties directly via Sepolia smart contracts. Page: https://arcprompts.lovable.app/ideas/photography-smart-photo-royalties-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Smart Photo Royalties" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Automate and transparently distribute photo royalties directly via Sepolia smart contracts. Discipline: Photography (royalty automation). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts enable automatic, fair, and transparent royalty payments without intermediaries. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SmartPhotoRoyalties.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SmartPhotoRoyalties /// @notice Automate and transparently distribute photo royalties directly via Sepolia smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SmartPhotoRoyalties { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SmartPhotoRoyalties` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SmartPhotoRoyalties.sol SmartPhotoRoyalties` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a royalty automation action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-smart-rights-marketplace-8 · photography · Smart Rights Marketplace ===== Pitch: Decentralized marketplace for buying and selling photo licenses managed by smart contracts. Page: https://arcprompts.lovable.app/ideas/photography-smart-rights-marketplace-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Smart Rights Marketplace" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Decentralized marketplace for buying and selling photo licenses managed by smart contracts. Discipline: Photography (licensing exchange). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts automate licensing terms and transfers securely and transparently. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SmartRightsMarketplace.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SmartRightsMarketplace /// @notice Decentralized marketplace for buying and selling photo licenses managed by smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SmartRightsMarketplace { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SmartRightsMarketplace` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SmartRightsMarketplace.sol SmartRightsMarketplace` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a licensing exchange action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-smart-watermark-registry-18 · photography · Smart Watermark Registry ===== Pitch: Register, track, and enforce photo watermarks rights via onchain smart contracts. Page: https://arcprompts.lovable.app/ideas/photography-smart-watermark-registry-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Smart Watermark Registry" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Register, track, and enforce photo watermarks rights via onchain smart contracts. Discipline: Photography (watermark management). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts provide provable ownership and watermark integrity enforcement. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SmartWatermarkRegistry.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SmartWatermarkRegistry /// @notice Register, track, and enforce photo watermarks rights via onchain smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SmartWatermarkRegistry { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SmartWatermarkRegistry` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SmartWatermarkRegistry.sol SmartWatermarkRegistry` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a watermark management action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-snapevent-auth-3 · photography · SnapEvent Auth ===== Pitch: Validate event photo ownership instantly with blockchain timestamps. Page: https://arcprompts.lovable.app/ideas/photography-snapevent-auth-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SnapEvent Auth" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Validate event photo ownership instantly with blockchain timestamps. Discipline: Photography (event photography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Timestamped NFT minting ensures event photo origin proof tied to IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SnapEventAuth.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SnapEventAuth /// @notice ERC-721 provenance for: Validate event photo ownership instantly with blockchain timestamps. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SnapEventAuth is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SnapEventAuth", "SNAPEV") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SnapEventAuth` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SnapEventAuth.sol SnapEventAuth` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a event photography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-snapshot-ledger-4 · photography · Snapshot Ledger ===== Pitch: Create a decentralized ledger of photo licenses pinned to IPFS for transparent usage rights. Page: https://arcprompts.lovable.app/ideas/photography-snapshot-ledger-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Snapshot Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create a decentralized ledger of photo licenses pinned to IPFS for transparent usage rights. Discipline: Photography (photo licensing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT pins licensing info securely with images, enabling immutable proof of rights. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SnapshotLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentSnapshotLedger /// @notice Create a decentralized ledger of photo licenses pinned to IPFS for transparent usage rights. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentSnapshotLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SnapshotLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SnapshotLedger.sol SnapshotLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a photo licensing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-sponsored-tx-model-releases-14 · photography · Sponsored Tx Model Releases ===== Pitch: Secure photo model releases onchain with gasless transactions for photographers and talent. Page: https://arcprompts.lovable.app/ideas/photography-sponsored-tx-model-releases-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sponsored Tx Model Releases" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Secure photo model releases onchain with gasless transactions for photographers and talent. Discipline: Photography (legal docs). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallets plus gasless tx simplify legal document signing and storage. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SponsoredTxModelReleases.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSponsoredTxModelReleases /// @notice Secure photo model releases onchain with gasless transactions for photographers and talent. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSponsoredTxModelReleases { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SponsoredTxModelReleases` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SponsoredTxModelReleases.sol SponsoredTxModelReleases` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every legal docs action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-sponsored-tx-photo-collages-24 · photography · Sponsored Tx Photo Collages ===== Pitch: Build and share photo collages onchain instantly without users paying gas fees. Page: https://arcprompts.lovable.app/ideas/photography-sponsored-tx-photo-collages-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sponsored Tx Photo Collages" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Build and share photo collages onchain instantly without users paying gas fees. Discipline: Photography (creative assembly). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet plus sponsored tx enable smooth collage minting. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SponsoredTxPhotoCollages.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSponsoredTxPhotoCollages /// @notice Build and share photo collages onchain instantly without users paying gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSponsoredTxPhotoCollages { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SponsoredTxPhotoCollages` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SponsoredTxPhotoCollages.sol SponsoredTxPhotoCollages` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every creative assembly action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-sponsored-tx-photo-sales-7 · photography · Sponsored Tx Photo Sales ===== Pitch: Sell your photos onchain with gasless transactions for buyers and sellers alike. Page: https://arcprompts.lovable.app/ideas/photography-sponsored-tx-photo-sales-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sponsored Tx Photo Sales" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Sell your photos onchain with gasless transactions for buyers and sellers alike. Discipline: Photography (photo marketplace). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored tx lower barriers ensuring frictionless photo marketplace experience. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SponsoredTxPhotoSales.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSponsoredTxPhotoSales /// @notice Sell your photos onchain with gasless transactions for buyers and sellers alike. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSponsoredTxPhotoSales { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SponsoredTxPhotoSales` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SponsoredTxPhotoSales.sol SponsoredTxPhotoSales` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every photo marketplace action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-sponsored-tx-photo-tips-18 · photography · Sponsored Tx Photo Tips ===== Pitch: Receive tips for your photography onchain without users paying gas fees. Page: https://arcprompts.lovable.app/ideas/photography-sponsored-tx-photo-tips-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sponsored Tx Photo Tips" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Receive tips for your photography onchain without users paying gas fees. Discipline: Photography (content monetization). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets plus sponsored tx deliver smooth, no-cost tipping experience. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SponsoredTxPhotoTips.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSponsoredTxPhotoTips /// @notice Receive tips for your photography onchain without users paying gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSponsoredTxPhotoTips { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SponsoredTxPhotoTips` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SponsoredTxPhotoTips.sol SponsoredTxPhotoTips` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every content monetization action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-stocksnap-rights-5 · photography · StockSnap Rights ===== Pitch: Secure and track photo stock licensing with NFT-based ownership proof. Page: https://arcprompts.lovable.app/ideas/photography-stocksnap-rights-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StockSnap Rights" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Secure and track photo stock licensing with NFT-based ownership proof. Discipline: Photography (stock photography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain tokens tie licenses and usage rights to IPFS-hosted photos. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StockSnapRights.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title StockSnapRights /// @notice ERC-721 provenance for: Secure and track photo stock licensing with NFT-based ownership proof. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StockSnapRights is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("StockSnapRights", "STOCKS") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StockSnapRights` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StockSnapRights.sol StockSnapRights` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a stock photography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-storyframe-ledger-20 · photography · StoryFrame Ledger ===== Pitch: Pin photo-story manifests on IPFS to secure multi-image narratives with permanent provenance. Page: https://arcprompts.lovable.app/ideas/photography-storyframe-ledger-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StoryFrame Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin photo-story manifests on IPFS to secure multi-image narratives with permanent provenance. Discipline: Photography (photo storytelling). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: JWT pinning creates immutable multi-photo linked manifests referenced by unique CIDs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StoryFrameLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentStoryFrameLedger /// @notice Pin photo-story manifests on IPFS to secure multi-image narratives with permanent provenance. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentStoryFrameLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StoryFrameLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StoryFrameLedger.sol StoryFrameLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a photo storytelling, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-timelapse-token-12 · photography · TimeLapse Token ===== Pitch: Record and prove time-lapse photo sequences provenance on blockchain. Page: https://arcprompts.lovable.app/ideas/photography-timelapse-token-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "TimeLapse Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record and prove time-lapse photo sequences provenance on blockchain. Discipline: Photography (time-lapse photography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs link IPFS stored time-lapse sequences with immutable ownership proof. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TimeLapseToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title TimeLapseToken /// @notice ERC-721 provenance for: Record and prove time-lapse photo sequences provenance on blockchain. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TimeLapseToken is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("TimeLapseToken", "TIMELA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TimeLapseToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TimeLapseToken.sol TimeLapseToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a time-lapse photography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-tip-jar-integration-14 · photography · Tip Jar Integration ===== Pitch: Enable fans to tip photographers instantly via Sepolia smart contract wallets. Page: https://arcprompts.lovable.app/ideas/photography-tip-jar-integration-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Tip Jar Integration" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable fans to tip photographers instantly via Sepolia smart contract wallets. Discipline: Photography (microdonations). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts facilitate direct, trustless transfer of microdonations with low fees. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TipJarIntegration.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title TipJarIntegration /// @notice Enable fans to tip photographers instantly via Sepolia smart contract wallets. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TipJarIntegration { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TipJarIntegration` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TipJarIntegration.sol TipJarIntegration` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a microdonations action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-tokenizelight-15 · photography · TokenizeLight ===== Pitch: Create tokens linked to photos pinned on IPFS for secure, transparent rights and royalty tracking. Page: https://arcprompts.lovable.app/ideas/photography-tokenizelight-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "TokenizeLight" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create tokens linked to photos pinned on IPFS for secure, transparent rights and royalty tracking. Discipline: Photography (rights management). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT creates durable IPFS records essential for tokenized royalty systems. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TokenizeLight.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentTokenizeLight /// @notice Create tokens linked to photos pinned on IPFS for secure, transparent rights and royalty tracking. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentTokenizeLight { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TokenizeLight` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TokenizeLight.sol TokenizeLight` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a rights management, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-trueshot-ledger-0 · photography · TrueShot Ledger ===== Pitch: Prove original photo ownership to combat unauthorized reuse and forgery. Page: https://arcprompts.lovable.app/ideas/photography-trueshot-ledger-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "TrueShot Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Prove original photo ownership to combat unauthorized reuse and forgery. Discipline: Photography (photo authenticity). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance mint creates immutable proof of original photo creation linked to IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TrueShotLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title TrueShotLedger /// @notice ERC-721 provenance for: Prove original photo ownership to combat unauthorized reuse and forgery. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TrueShotLedger is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("TrueShotLedger", "TRUESH") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TrueShotLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TrueShotLedger.sol TrueShotLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a photo authenticity, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-urbanview-token-23 · photography · UrbanView Token ===== Pitch: Authenticate urban scene photos for creators and licensing platforms. Page: https://arcprompts.lovable.app/ideas/photography-urbanview-token-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "UrbanView Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate urban scene photos for creators and licensing platforms. Discipline: Photography (urban photography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 minting verifies unique IPFS urban photo ownership records. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/UrbanViewToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title UrbanViewToken /// @notice ERC-721 provenance for: Authenticate urban scene photos for creators and licensing platforms. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract UrbanViewToken is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("UrbanViewToken", "URBANV") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs UrbanViewToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/UrbanViewToken.sol UrbanViewToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a urban photography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-verified-client-contracts-13 · photography · Verified Client Contracts ===== Pitch: Draft and execute photo service contracts on Sepolia smart contracts, reducing disputes. Page: https://arcprompts.lovable.app/ideas/photography-verified-client-contracts-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Verified Client Contracts" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Draft and execute photo service contracts on Sepolia smart contracts, reducing disputes. Discipline: Photography (client agreements). Onchain primitive: Arc smart contract. Why this primitive: Onchain contracts provide immutable, verifiable proof of agreed terms. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VerifiedClientContracts.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title VerifiedClientContracts /// @notice Draft and execute photo service contracts on Sepolia smart contracts, reducing disputes. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VerifiedClientContracts { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VerifiedClientContracts` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VerifiedClientContracts.sol VerifiedClientContracts` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a client agreements action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-vintagevibe-mint-8 · photography · VintageVibe Mint ===== Pitch: Digitally mint vintage photo scans to preserve original source provenance. Page: https://arcprompts.lovable.app/ideas/photography-vintagevibe-mint-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VintageVibe Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Digitally mint vintage photo scans to preserve original source provenance. Discipline: Photography (vintage photography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs link vintage photo IPFS assets to creator-defined provenance records. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VintageVibeMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title VintageVibeMint /// @notice ERC-721 provenance for: Digitally mint vintage photo scans to preserve original source provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VintageVibeMint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("VintageVibeMint", "VINTAG") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VintageVibeMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VintageVibeMint.sol VintageVibeMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a vintage photography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA photography-wildlifemint-ledger-20 · photography · WildlifeMint Ledger ===== Pitch: Authenticate unique wildlife photos with immutable NFT provenance tokens. Page: https://arcprompts.lovable.app/ideas/photography-wildlifemint-ledger-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "WildlifeMint Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate unique wildlife photos with immutable NFT provenance tokens. Discipline: Photography (wildlife photography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 records verified ownership linked to IPFS wildlife imagery. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/WildlifeMintLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title WildlifeMintLedger /// @notice ERC-721 provenance for: Authenticate unique wildlife photos with immutable NFT provenance tokens. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract WildlifeMintLedger is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("WildlifeMintLedger", "WILDLI") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs WildlifeMintLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/WildlifeMintLedger.sol WildlifeMintLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a wildlife photography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-actor-rehearsal-logs-2 · theater · Actor Rehearsal Logs ===== Pitch: Actors mint daily rehearsal videos as NFTs to prove practice and progress. Page: https://arcprompts.lovable.app/ideas/theater-actor-rehearsal-logs-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Actor Rehearsal Logs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Actors mint daily rehearsal videos as NFTs to prove practice and progress. Discipline: Theater & Live Performance (performance tracking). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain minting timestamps rehearsal footage with verified ownership by performers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ActorRehearsalLogs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ActorRehearsalLogs /// @notice ERC-721 provenance for: Actors mint daily rehearsal videos as NFTs to prove practice and progress. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ActorRehearsalLogs is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ActorRehearsalLogs", "ACTORR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ActorRehearsalLogs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ActorRehearsalLogs.sol ActorRehearsalLogs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a performance tracking, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-actorlinetracker-22 · theater · ActorLineTracker ===== Pitch: Pin annotated lines and rehearsal notes as immutable JSON to IPFS for actors’ reliable study aids. Page: https://arcprompts.lovable.app/ideas/theater-actorlinetracker-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ActorLineTracker" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin annotated lines and rehearsal notes as immutable JSON to IPFS for actors’ reliable study aids. Discipline: Theater & Live Performance (line memorization). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata enables secure, permanent pinning of rehearsal content accessible across devices. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ActorLineTracker.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentActorLineTracker /// @notice Pin annotated lines and rehearsal notes as immutable JSON to IPFS for actors’ reliable study aids. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentActorLineTracker { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ActorLineTracker` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ActorLineTracker.sol ActorLineTracker` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a line memorization, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-actormint-ledger-15 · theater · ActorMint Ledger ===== Pitch: Mint and distribute unique actor NFTs representing roles and performances with gasless minting. Page: https://arcprompts.lovable.app/ideas/theater-actormint-ledger-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ActorMint Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint and distribute unique actor NFTs representing roles and performances with gasless minting. Discipline: Theater & Live Performance (performance NFTs). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social verifies actors; sponsored tx remove minting friction. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ActorMintLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogActorMintLedger /// @notice Mint and distribute unique actor NFTs representing roles and performances with gasless minting. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogActorMintLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ActorMintLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ActorMintLedger.sol ActorMintLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every performance NFTs action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-actorportfolio-hub-3 · theater · ActorPortfolio Hub ===== Pitch: Actors upload and pin portfolios including headshots and performance clips permanently on IPFS for casting accessibility. Page: https://arcprompts.lovable.app/ideas/theater-actorportfolio-hub-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ActorPortfolio Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Actors upload and pin portfolios including headshots and performance clips permanently on IPFS for casting accessibility. Discipline: Theater & Live Performance (performance showcase). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS via Pinata ensures persistent availability and integrity of actor showcase content. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ActorPortfolioHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentActorPortfolioHub /// @notice Actors upload and pin portfolios including headshots and performance clips permanently on IPFS for casting accessibility. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentActorPortfolioHub { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ActorPortfolioHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ActorPortfolioHub.sol ActorPortfolioHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a performance showcase, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-actorstake-platform-9 · theater · ActorStake Platform ===== Pitch: Allow fans to stake tokens on actors’ performances, rewarding success with blockchain incentives. Page: https://arcprompts.lovable.app/ideas/theater-actorstake-platform-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ActorStake Platform" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Allow fans to stake tokens on actors’ performances, rewarding success with blockchain incentives. Discipline: Theater & Live Performance (performance staking). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts manage stakes and payouts transparently for audience engagement. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ActorStakePlatform.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ActorStakePlatform /// @notice Allow fans to stake tokens on actors’ performances, rewarding success with blockchain incentives. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ActorStakePlatform { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ActorStakePlatform` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ActorStakePlatform.sol ActorStakePlatform` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a performance staking action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-actortrust-ledger-4 · theater · ActorTrust Ledger ===== Pitch: Store and verify actor resumes and roles securely with social login and no gas fees. Page: https://arcprompts.lovable.app/ideas/theater-actortrust-ledger-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ActorTrust Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store and verify actor resumes and roles securely with social login and no gas fees. Discipline: Theater & Live Performance (performance credentials). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social offers identity verification; sponsored tx allow easy credential updates. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ActorTrustLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogActorTrustLedger /// @notice Store and verify actor resumes and roles securely with social login and no gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogActorTrustLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ActorTrustLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ActorTrustLedger.sol ActorTrustLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every performance credentials action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-audience-experience-logs-15 · theater · Audience Experience Logs ===== Pitch: Mint audience reaction recordings as NFTs to capture genuine engagement data. Page: https://arcprompts.lovable.app/ideas/theater-audience-experience-logs-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Audience Experience Logs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint audience reaction recordings as NFTs to capture genuine engagement data. Discipline: Theater & Live Performance (audience analytics). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs provide verifiable proof of authentic audience feedback content. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AudienceExperienceLogs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title AudienceExperienceLogs /// @notice ERC-721 provenance for: Mint audience reaction recordings as NFTs to capture genuine engagement data. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AudienceExperienceLogs is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("AudienceExperienceLogs", "AUDIEN") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AudienceExperienceLogs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AudienceExperienceLogs.sol AudienceExperienceLogs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a audience analytics, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-audiencetip-jar-19 · theater · AudienceTip Jar ===== Pitch: Let audiences tip performers instantly with social wallets and sponsored transaction gaslessness. Page: https://arcprompts.lovable.app/ideas/theater-audiencetip-jar-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AudienceTip Jar" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Let audiences tip performers instantly with social wallets and sponsored transaction gaslessness. Discipline: Theater & Live Performance (fan monetization). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social facilitates fan identity; sponsored tx enable free tips. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AudienceTipJar.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogAudienceTipJar /// @notice Let audiences tip performers instantly with social wallets and sponsored transaction gaslessness. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogAudienceTipJar { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AudienceTipJar` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AudienceTipJar.sol AudienceTipJar` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every fan monetization action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-audiencevote-dao-13 · theater · AudienceVote DAO ===== Pitch: Collect and tally audience votes on performances in a transparent, tamper-proof DAO system. Page: https://arcprompts.lovable.app/ideas/theater-audiencevote-dao-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AudienceVote DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Collect and tally audience votes on performances in a transparent, tamper-proof DAO system. Discipline: Theater & Live Performance (live feedback). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contract ensures verified, unbiased audience participation results instantly. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AudienceVoteDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AudienceVoteDAO /// @notice Collect and tally audience votes on performances in a transparent, tamper-proof DAO system. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AudienceVoteDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AudienceVoteDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AudienceVoteDAO.sol AudienceVoteDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a live feedback action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-audiencevote-live-7 · theater · AudienceVote Live ===== Pitch: Let audiences vote live on performance elements using social login and gasless onchain votes. Page: https://arcprompts.lovable.app/ideas/theater-audiencevote-live-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AudienceVote Live" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Let audiences vote live on performance elements using social login and gasless onchain votes. Discipline: Theater & Live Performance (interactive performances). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social verifies unique voters; sponsored tx ensure smooth, no-fee tallying. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AudienceVoteLive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogAudienceVoteLive /// @notice Let audiences vote live on performance elements using social login and gasless onchain votes. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogAudienceVoteLive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AudienceVoteLive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AudienceVoteLive.sol AudienceVoteLive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every interactive performances action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-backstage-pass-8 · theater · Backstage Pass ===== Pitch: Offer backstage digital passes with seamless social wallet login and free sponsorship tx. Page: https://arcprompts.lovable.app/ideas/theater-backstage-pass-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Backstage Pass" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Offer backstage digital passes with seamless social wallet login and free sponsorship tx. Discipline: Theater & Live Performance (exclusive access). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social ensures verified fans; sponsored tx keep access delivery fee-free. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/BackstagePass.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogBackstagePass /// @notice Offer backstage digital passes with seamless social wallet login and free sponsorship tx. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogBackstagePass { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs BackstagePass` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/BackstagePass.sol BackstagePass` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every exclusive access action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-backstage-token-14 · theater · Backstage Token ===== Pitch: Issue tokens granting secure, onchain backstage access for staff and VIPs. Page: https://arcprompts.lovable.app/ideas/theater-backstage-token-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Backstage Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Issue tokens granting secure, onchain backstage access for staff and VIPs. Discipline: Theater & Live Performance (access control). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide verifiable, transferable digital credentials for event access control. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/BackstageToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title BackstageToken /// @notice Issue tokens granting secure, onchain backstage access for staff and VIPs. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract BackstageToken { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs BackstageToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/BackstageToken.sol BackstageToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a access control action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-caststake-dao-20 · theater · CastStake DAO ===== Pitch: Decentralize theater production funding by letting stakeholders vote and invest through DAO tokens. Page: https://arcprompts.lovable.app/ideas/theater-caststake-dao-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "CastStake DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Decentralize theater production funding by letting stakeholders vote and invest through DAO tokens. Discipline: Theater & Live Performance (fundraising). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts ensure transparent disbursement and stakeholder governance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CastStakeDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title CastStakeDAO /// @notice Decentralize theater production funding by letting stakeholders vote and invest through DAO tokens. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CastStakeDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CastStakeDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CastStakeDAO.sol CastStakeDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a fundraising action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-choreography-chain-12 · theater · Choreography Chain ===== Pitch: Mint choreography sequences as NFTs ensuring original dance and movement rights. Page: https://arcprompts.lovable.app/ideas/theater-choreography-chain-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Choreography Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint choreography sequences as NFTs ensuring original dance and movement rights. Discipline: Theater & Live Performance (movement design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs record and timestamp choreography with creator attribution. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChoreographyChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ChoreographyChain /// @notice ERC-721 provenance for: Mint choreography sequences as NFTs ensuring original dance and movement rights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ChoreographyChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ChoreographyChain", "CHOREO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChoreographyChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChoreographyChain.sol ChoreographyChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a movement design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-costume-provenance-chain-6 · theater · Costume Provenance Chain ===== Pitch: Mint costume designs and ownership as NFTs to protect designer rights and provenance. Page: https://arcprompts.lovable.app/ideas/theater-costume-provenance-chain-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Costume Provenance Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint costume designs and ownership as NFTs to protect designer rights and provenance. Discipline: Theater & Live Performance (costume design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs secure costume intellectual property with explicit creator ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CostumeProvenanceChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title CostumeProvenanceChain /// @notice ERC-721 provenance for: Mint costume designs and ownership as NFTs to protect designer rights and provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CostumeProvenanceChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("CostumeProvenanceChain", "COSTUM") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CostumeProvenanceChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CostumeProvenanceChain.sol CostumeProvenanceChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a costume design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-costume-rental-nft-20 · theater · Costume Rental NFT ===== Pitch: Mint NFTs representing costume rental agreements to streamline transactions. Page: https://arcprompts.lovable.app/ideas/theater-costume-rental-nft-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Costume Rental NFT" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs representing costume rental agreements to streamline transactions. Discipline: Theater & Live Performance (rental services). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs enable trustless rental contracts with provenance and transferability. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CostumeRentalNFT.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title CostumeRentalNFT /// @notice ERC-721 provenance for: Mint NFTs representing costume rental agreements to streamline transactions. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CostumeRentalNFT is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("CostumeRentalNFT", "COSTUM") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CostumeRentalNFT` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CostumeRentalNFT.sol CostumeRentalNFT` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a rental services, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-costumemood-board-2 · theater · CostumeMood Board ===== Pitch: Pin and share costume sketches and mood boards on IPFS for collaborative costume creation. Page: https://arcprompts.lovable.app/ideas/theater-costumemood-board-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "CostumeMood Board" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin and share costume sketches and mood boards on IPFS for collaborative costume creation. Discipline: Theater & Live Performance (costume design). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata enables persistent, tamper-proof storage of costume design images and metadata. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CostumeMoodBoard.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentCostumeMoodBoard /// @notice Pin and share costume sketches and mood boards on IPFS for collaborative costume creation. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentCostumeMoodBoard { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CostumeMoodBoard` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CostumeMoodBoard.sol CostumeMoodBoard` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a costume design, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-costumetexture-bank-14 · theater · CostumeTexture Bank ===== Pitch: Pin high-resolution fabric texture images and details on IPFS for costume designers’ resource sharing. Page: https://arcprompts.lovable.app/ideas/theater-costumetexture-bank-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "CostumeTexture Bank" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin high-resolution fabric texture images and details on IPFS for costume designers’ resource sharing. Discipline: Theater & Live Performance (fabric study). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s IPFS pinning preserves image quality and metadata for collaborative costume fabrics. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CostumeTextureBank.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentCostumeTextureBank /// @notice Pin high-resolution fabric texture images and details on IPFS for costume designers’ resource sharing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentCostumeTextureBank { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CostumeTextureBank` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CostumeTextureBank.sol CostumeTextureBank` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a fabric study, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-critiquechain-feedback-23 · theater · CritiqueChain Feedback ===== Pitch: Collect verified audience critiques onchain with social login and no-fee feedback transactions. Page: https://arcprompts.lovable.app/ideas/theater-critiquechain-feedback-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "CritiqueChain Feedback" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Collect verified audience critiques onchain with social login and no-fee feedback transactions. Discipline: Theater & Live Performance (performance reviews). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social verifies identities; sponsored tx make feedback free and easy. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CritiqueChainFeedback.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogCritiqueChainFeedback /// @notice Collect verified audience critiques onchain with social login and no-fee feedback transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogCritiqueChainFeedback { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CritiqueChainFeedback` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CritiqueChainFeedback.sol CritiqueChainFeedback` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every performance reviews action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-cuepoint-tracker-14 · theater · CuePoint Tracker ===== Pitch: Provide directors with gasless onchain cue tracking and adjustments via social login. Page: https://arcprompts.lovable.app/ideas/theater-cuepoint-tracker-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "CuePoint Tracker" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Provide directors with gasless onchain cue tracking and adjustments via social login. Discipline: Theater & Live Performance (live direction). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social manages secure access; sponsored tx enable instant, free cue updates. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CuePointTracker.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogCuePointTracker /// @notice Provide directors with gasless onchain cue tracking and adjustments via social login. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogCuePointTracker { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CuePointTracker` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CuePointTracker.sol CuePointTracker` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every live direction action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-director-s-vision-ledger-9 · theater · Director's Vision Ledger ===== Pitch: Directors mint annotated scripts as NFTs capturing their unique vision and notes. Page: https://arcprompts.lovable.app/ideas/theater-director-s-vision-ledger-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Director's Vision Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Directors mint annotated scripts as NFTs capturing their unique vision and notes. Discipline: Theater & Live Performance (direction). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs record the director’s creative input with provenance and ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DirectorsVisionLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title DirectorsVisionLedger /// @notice ERC-721 provenance for: Directors mint annotated scripts as NFTs capturing their unique vision and notes. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DirectorsVisionLedger is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("DirectorsVisionLedger", "DIRECT") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DirectorsVisionLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DirectorsVisionLedger.sol DirectorsVisionLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a direction, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-directorstoryboard-hub-16 · theater · DirectorStoryboard Hub ===== Pitch: Pin storyboards as image sequences on IPFS to maintain integrity and facilitate collaboration across teams. Page: https://arcprompts.lovable.app/ideas/theater-directorstoryboard-hub-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "DirectorStoryboard Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin storyboards as image sequences on IPFS to maintain integrity and facilitate collaboration across teams. Discipline: Theater & Live Performance (storyboarding). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata enables permanent IPFS pinning of multi-image storyboards with reliable CID referencing. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DirectorStoryboardHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentDirectorStoryboardHub /// @notice Pin storyboards as image sequences on IPFS to maintain integrity and facilitate collaboration across teams. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentDirectorStoryboardHub { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DirectorStoryboardHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DirectorStoryboardHub.sol DirectorStoryboardHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a storyboarding, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-drama-therapy-journals-19 · theater · Drama Therapy Journals ===== Pitch: Mint personal drama therapy session outputs as NFTs to secure privacy and ownership. Page: https://arcprompts.lovable.app/ideas/theater-drama-therapy-journals-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Drama Therapy Journals" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint personal drama therapy session outputs as NFTs to secure privacy and ownership. Discipline: Theater & Live Performance (therapeutic performance). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs provide confidential, verifiable proof of therapeutic creative work. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DramaTherapyJournals.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title DramaTherapyJournals /// @notice ERC-721 provenance for: Mint personal drama therapy session outputs as NFTs to secure privacy and ownership. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DramaTherapyJournals is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("DramaTherapyJournals", "DRAMAT") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DramaTherapyJournals` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DramaTherapyJournals.sol DramaTherapyJournals` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a therapeutic performance, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-encore-reward-23 · theater · Encore Reward ===== Pitch: Tokenize audience rewards to incentivize repeat attendance and engagement at live performances. Page: https://arcprompts.lovable.app/ideas/theater-encore-reward-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Encore Reward" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Tokenize audience rewards to incentivize repeat attendance and engagement at live performances. Discipline: Theater & Live Performance (audience loyalty). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts manage transparent loyalty point issuance and redemption. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EncoreReward.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title EncoreReward /// @notice Tokenize audience rewards to incentivize repeat attendance and engagement at live performances. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract EncoreReward { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EncoreReward` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EncoreReward.sol EncoreReward` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a audience loyalty action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-improv-moment-mint-11 · theater · Improv Moment Mint ===== Pitch: Actors mint spontaneous improv performances as NFTs to preserve unique moments. Page: https://arcprompts.lovable.app/ideas/theater-improv-moment-mint-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Improv Moment Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Actors mint spontaneous improv performances as NFTs to preserve unique moments. Discipline: Theater & Live Performance (improvisation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance captures ephemeral content with verifiable ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ImprovMomentMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ImprovMomentMint /// @notice ERC-721 provenance for: Actors mint spontaneous improv performances as NFTs to preserve unique moments. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ImprovMomentMint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ImprovMomentMint", "IMPROV") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ImprovMomentMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ImprovMomentMint.sol ImprovMomentMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a improvisation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-interactive-stage-nfts-10 · theater · Interactive Stage NFTs ===== Pitch: Mint interactive performance elements as NFTs to prove creative ownership. Page: https://arcprompts.lovable.app/ideas/theater-interactive-stage-nfts-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Interactive Stage NFTs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint interactive performance elements as NFTs to prove creative ownership. Discipline: Theater & Live Performance (stage interaction). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs authenticate unique interactive stage designs and technology. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InteractiveStageNFTs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title InteractiveStageNFTs /// @notice ERC-721 provenance for: Mint interactive performance elements as NFTs to prove creative ownership. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract InteractiveStageNFTs is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("InteractiveStageNFTs", "INTERA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InteractiveStageNFTs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InteractiveStageNFTs.sol InteractiveStageNFTs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a stage interaction, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-lightcue-automation-10 · theater · LightCue Automation ===== Pitch: Record and trigger lighting cues onchain to verify show timing and enable automated playback. Page: https://arcprompts.lovable.app/ideas/theater-lightcue-automation-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LightCue Automation" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record and trigger lighting cues onchain to verify show timing and enable automated playback. Discipline: Theater & Live Performance (lighting cue logs). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts guarantee immutable cue sequences and decentralize control of lighting shows. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LightCueAutomation.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title LightCueAutomation /// @notice Record and trigger lighting cues onchain to verify show timing and enable automated playback. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract LightCueAutomation { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LightCueAutomation` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LightCueAutomation.sol LightCueAutomation` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a lighting cue logs action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-lightcue-sync-3 · theater · LightCue Sync ===== Pitch: Collaborate on lighting cues live with instant, gasless onchain updates through social login. Page: https://arcprompts.lovable.app/ideas/theater-lightcue-sync-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LightCue Sync" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Collaborate on lighting cues live with instant, gasless onchain updates through social login. Discipline: Theater & Live Performance (lighting design). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social streamlines designer access; sponsored transactions enable real-time updates. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LightCueSync.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogLightCueSync /// @notice Collaborate on lighting cues live with instant, gasless onchain updates through social login. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogLightCueSync { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LightCueSync` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LightCueSync.sol LightCueSync` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every lighting design action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-lightfx-tokenize-19 · theater · LightFX Tokenize ===== Pitch: Tokenize and license stage special effects designs to productions worldwide securely. Page: https://arcprompts.lovable.app/ideas/theater-lightfx-tokenize-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LightFX Tokenize" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Tokenize and license stage special effects designs to productions worldwide securely. Discipline: Theater & Live Performance (special effects licensing). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enable transparent rights management for special effects creators. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LightFXTokenize.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title LightFXTokenize /// @notice Tokenize and license stage special effects designs to productions worldwide securely. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract LightFXTokenize { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LightFXTokenize` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LightFXTokenize.sol LightFXTokenize` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a special effects licensing action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-lighting-cue-chain-1 · theater · Lighting Cue Chain ===== Pitch: Record and verify lighting cue sequences as unique NFTs for live performances. Page: https://arcprompts.lovable.app/ideas/theater-lighting-cue-chain-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Lighting Cue Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record and verify lighting cue sequences as unique NFTs for live performances. Discipline: Theater & Live Performance (lighting design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs provide verifiable provenance for dynamic stage lighting designs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LightingCueChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title LightingCueChain /// @notice ERC-721 provenance for: Record and verify lighting cue sequences as unique NFTs for live performances. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract LightingCueChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("LightingCueChain", "LIGHTI") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LightingCueChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LightingCueChain.sol LightingCueChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a lighting design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-lightingcues-ledger-5 · theater · LightingCues Ledger ===== Pitch: Save and distribute lighting cue sequences as pinned JSON on IPFS for synchronized performances. Page: https://arcprompts.lovable.app/ideas/theater-lightingcues-ledger-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LightingCues Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Save and distribute lighting cue sequences as pinned JSON on IPFS for synchronized performances. Discipline: Theater & Live Performance (lighting cues). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata allows secure, permanent pinning of cue sequences accessible to all crew members. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LightingCuesLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentLightingCuesLedger /// @notice Save and distribute lighting cue sequences as pinned JSON on IPFS for synchronized performances. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentLightingCuesLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LightingCuesLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LightingCuesLedger.sol LightingCuesLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a lighting cues, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-lightingfixture-db-17 · theater · LightingFixture DB ===== Pitch: Pin detailed lighting fixture profiles and manuals on IPFS for technicians and designers to access anytime. Page: https://arcprompts.lovable.app/ideas/theater-lightingfixture-db-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LightingFixture DB" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin detailed lighting fixture profiles and manuals on IPFS for technicians and designers to access anytime. Discipline: Theater & Live Performance (equipment catalog). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata secures fixture data immutably with CID links ensuring decentralization and longevity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LightingFixtureDB.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentLightingFixtureDB /// @notice Pin detailed lighting fixture profiles and manuals on IPFS for technicians and designers to access anytime. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentLightingFixtureDB { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LightingFixtureDB` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LightingFixtureDB.sol LightingFixtureDB` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a equipment catalog, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-lightingpalette-exchange-11 · theater · LightingPalette Exchange ===== Pitch: Pin and trade curated lighting color palettes as images and data on IPFS for designers to reuse. Page: https://arcprompts.lovable.app/ideas/theater-lightingpalette-exchange-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LightingPalette Exchange" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin and trade curated lighting color palettes as images and data on IPFS for designers to reuse. Discipline: Theater & Live Performance (color study). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s JWT upload ensures palettes are permanently stored and shareable with CID references. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LightingPaletteExchange.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentLightingPaletteExchange /// @notice Pin and trade curated lighting color palettes as images and data on IPFS for designers to reuse. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentLightingPaletteExchange { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LightingPaletteExchange` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LightingPaletteExchange.sol LightingPaletteExchange` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a color study, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-lightingpattern-archive-19 · theater · LightingPattern Archive ===== Pitch: Pin and share choreographed lighting patterns and sequences immutably on IPFS for repeatable productions. Page: https://arcprompts.lovable.app/ideas/theater-lightingpattern-archive-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LightingPattern Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin and share choreographed lighting patterns and sequences immutably on IPFS for repeatable productions. Discipline: Theater & Live Performance (lighting choreography). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata ensures choreography data is stored permanently for consistent performance replication. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LightingPatternArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentLightingPatternArchive /// @notice Pin and share choreographed lighting patterns and sequences immutably on IPFS for repeatable productions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentLightingPatternArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LightingPatternArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LightingPatternArchive.sol LightingPatternArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a lighting choreography, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-lightingtoken-rewards-17 · theater · LightingToken Rewards ===== Pitch: Issue tokenized rewards to lighting crews with social wallet onboarding and sponsored tx. Page: https://arcprompts.lovable.app/ideas/theater-lightingtoken-rewards-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LightingToken Rewards" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Issue tokenized rewards to lighting crews with social wallet onboarding and sponsored tx. Discipline: Theater & Live Performance (crew incentives). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social authenticates recipients; sponsored tx enable frictionless token transfers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LightingTokenRewards.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogLightingTokenRewards /// @notice Issue tokenized rewards to lighting crews with social wallet onboarding and sponsored tx. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogLightingTokenRewards { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LightingTokenRewards` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LightingTokenRewards.sol LightingTokenRewards` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every crew incentives action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-monologue-archive-13 · theater · Monologue Archive ===== Pitch: Pin curated monologue scripts and performance notes on IPFS preserving artistic intent unchangeably. Page: https://arcprompts.lovable.app/ideas/theater-monologue-archive-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Monologue Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin curated monologue scripts and performance notes on IPFS preserving artistic intent unchangeably. Discipline: Theater & Live Performance (performance repertoire). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata ensures monologue content is stored immutable with permanent CID links accessible worldwide. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MonologueArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMonologueArchive /// @notice Pin curated monologue scripts and performance notes on IPFS preserving artistic intent unchangeably. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMonologueArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MonologueArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MonologueArchive.sol MonologueArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a performance repertoire, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-monologue-minting-hub-7 · theater · Monologue Minting Hub ===== Pitch: Actors mint monologues as NFTs to showcase and monetize original performances. Page: https://arcprompts.lovable.app/ideas/theater-monologue-minting-hub-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Monologue Minting Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Actors mint monologues as NFTs to showcase and monetize original performances. Discipline: Theater & Live Performance (acting). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs verify creators’ unique performance content and intellectual property. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MonologueMintingHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MonologueMintingHub /// @notice ERC-721 provenance for: Actors mint monologues as NFTs to showcase and monetize original performances. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MonologueMintingHub is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MonologueMintingHub", "MONOLO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MonologueMintingHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MonologueMintingHub.sol MonologueMintingHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a acting, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-performance-highlight-reels-22 · theater · Performance Highlight Reels ===== Pitch: Actors mint highlight reels as NFTs to prove originality and attract bookings. Page: https://arcprompts.lovable.app/ideas/theater-performance-highlight-reels-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Performance Highlight Reels" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Actors mint highlight reels as NFTs to prove originality and attract bookings. Discipline: Theater & Live Performance (promotion). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance confirms authentic content ownership for self-promotion. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PerformanceHighlightReels.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PerformanceHighlightReels /// @notice ERC-721 provenance for: Actors mint highlight reels as NFTs to prove originality and attract bookings. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PerformanceHighlightReels is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PerformanceHighlightReels", "PERFOR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PerformanceHighlightReels` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PerformanceHighlightReels.sol PerformanceHighlightReels` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a promotion, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-performance-nft-tickets-5 · theater · Performance NFT Tickets ===== Pitch: Issue NFT tickets proving legitimate access and unique attendance experience. Page: https://arcprompts.lovable.app/ideas/theater-performance-nft-tickets-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Performance NFT Tickets" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Issue NFT tickets proving legitimate access and unique attendance experience. Discipline: Theater & Live Performance (audience engagement). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT tickets provide tamper-proof proof of participation and ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PerformanceNFTTickets.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PerformanceNFTTickets /// @notice ERC-721 provenance for: Issue NFT tickets proving legitimate access and unique attendance experience. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PerformanceNFTTickets is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PerformanceNFTTickets", "PERFOR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PerformanceNFTTickets` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PerformanceNFTTickets.sol PerformanceNFTTickets` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a audience engagement, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-performancemanifesto-20 · theater · PerformanceManifesto ===== Pitch: Pin performance manifestos and artistic statements as permanent JSON files on IPFS for posterity and access. Page: https://arcprompts.lovable.app/ideas/theater-performancemanifesto-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PerformanceManifesto" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin performance manifestos and artistic statements as permanent JSON files on IPFS for posterity and access. Discipline: Theater & Live Performance (creative documentation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s IPFS pinning offers an immutable record for artists’ creative intent and public sharing. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PerformanceManifesto.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPerformanceManifesto /// @notice Pin performance manifestos and artistic statements as permanent JSON files on IPFS for posterity and access. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPerformanceManifesto { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PerformanceManifesto` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PerformanceManifesto.sol PerformanceManifesto` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a creative documentation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-performancereview-ledger-10 · theater · PerformanceReview Ledger ===== Pitch: Store aggregated audience reviews as pinned JSON on IPFS guaranteeing unalterable feedback records. Page: https://arcprompts.lovable.app/ideas/theater-performancereview-ledger-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PerformanceReview Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store aggregated audience reviews as pinned JSON on IPFS guaranteeing unalterable feedback records. Discipline: Theater & Live Performance (audience feedback). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata pins feedback data securely, ensuring transparency and trust in review storage. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PerformanceReviewLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPerformanceReviewLedger /// @notice Store aggregated audience reviews as pinned JSON on IPFS guaranteeing unalterable feedback records. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPerformanceReviewLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PerformanceReviewLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PerformanceReviewLedger.sol PerformanceReviewLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a audience feedback, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-performerbadge-nft-18 · theater · PerformerBadge NFT ===== Pitch: Mint verifiable NFT badges certifying performer skills and achievements. Page: https://arcprompts.lovable.app/ideas/theater-performerbadge-nft-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PerformerBadge NFT" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint verifiable NFT badges certifying performer skills and achievements. Discipline: Theater & Live Performance (credentials verification). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts provide immutable proof of credentials within the live performance industry. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PerformerBadgeNFT.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PerformerBadgeNFT /// @notice Mint verifiable NFT badges certifying performer skills and achievements. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PerformerBadgeNFT { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PerformerBadgeNFT` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PerformerBadgeNFT.sol PerformerBadgeNFT` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a credentials verification action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-playbill-archive-24 · theater · PlayBill Archive ===== Pitch: Pin historic and current playbill designs on IPFS for theatrical heritage and design reference. Page: https://arcprompts.lovable.app/ideas/theater-playbill-archive-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PlayBill Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin historic and current playbill designs on IPFS for theatrical heritage and design reference. Discipline: Theater & Live Performance (program design). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata offers permanent, decentralized hosting of program designs as image and JSON manifests. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlayBillArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPlayBillArchive /// @notice Pin historic and current playbill designs on IPFS for theatrical heritage and design reference. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPlayBillArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlayBillArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlayBillArchive.sol PlayBillArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a program design, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-playbill-ledger-12 · theater · Playbill Ledger ===== Pitch: Securely record production credits and changes transparently on a public ledger. Page: https://arcprompts.lovable.app/ideas/theater-playbill-ledger-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Playbill Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely record production credits and changes transparently on a public ledger. Discipline: Theater & Live Performance (production documentation). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enable immutable, timestamped records of live performance production versions. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlaybillLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PlaybillLedger /// @notice Securely record production credits and changes transparently on a public ledger. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PlaybillLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlaybillLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlaybillLedger.sol PlaybillLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a production documentation action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-playbill-nfts-6 · theater · Playbill NFTs ===== Pitch: Create and distribute collectible digital playbills with embedded social wallet and free minting. Page: https://arcprompts.lovable.app/ideas/theater-playbill-nfts-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Playbill NFTs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create and distribute collectible digital playbills with embedded social wallet and free minting. Discipline: Theater & Live Performance (memorabilia). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social enables easy user onboarding; sponsored tx remove minting friction. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlaybillNFTs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPlaybillNFTs /// @notice Create and distribute collectible digital playbills with embedded social wallet and free minting. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPlaybillNFTs { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlaybillNFTs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlaybillNFTs.sol PlaybillNFTs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every memorabilia action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-playbill-provenance-17 · theater · Playbill Provenance ===== Pitch: Mint digital playbills as NFTs to preserve historical performance records. Page: https://arcprompts.lovable.app/ideas/theater-playbill-provenance-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Playbill Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint digital playbills as NFTs to preserve historical performance records. Discipline: Theater & Live Performance (program design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs store immutable playbill data with creator proof. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlaybillProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PlaybillProvenance /// @notice ERC-721 provenance for: Mint digital playbills as NFTs to preserve historical performance records. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PlaybillProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PlaybillProvenance", "PLAYBI") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlaybillProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlaybillProvenance.sol PlaybillProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a program design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-playmint-nft-8 · theater · PlayMint NFT ===== Pitch: Mint unique digital editions of scripts as NFTs to protect intellectual property and track sales. Page: https://arcprompts.lovable.app/ideas/theater-playmint-nft-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PlayMint NFT" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint unique digital editions of scripts as NFTs to protect intellectual property and track sales. Discipline: Theater & Live Performance (script NFTs). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide verifiable ownership and provenance for playwrights’ works. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlayMintNFT.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PlayMintNFT /// @notice Mint unique digital editions of scripts as NFTs to protect intellectual property and track sales. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PlayMintNFT { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlayMintNFT` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlayMintNFT.sol PlayMintNFT` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a script NFTs action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-playpromo-kit-6 · theater · PlayPromo Kit ===== Pitch: Pin play promo images and metadata on IPFS to ensure immutable, globally accessible marketing assets. Page: https://arcprompts.lovable.app/ideas/theater-playpromo-kit-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PlayPromo Kit" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin play promo images and metadata on IPFS to ensure immutable, globally accessible marketing assets. Discipline: Theater & Live Performance (marketing collateral). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata provides permanent CID links ensuring marketing materials remain hosted reliably. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlayPromoKit.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPlayPromoKit /// @notice Pin play promo images and metadata on IPFS to ensure immutable, globally accessible marketing assets. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPlayPromoKit { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlayPromoKit` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlayPromoKit.sol PlayPromoKit` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a marketing collateral, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-playscript-vault-1 · theater · PlayScript Vault ===== Pitch: Securely save, share, and version play scripts on IPFS to preserve authorship and facilitate collaboration. Page: https://arcprompts.lovable.app/ideas/theater-playscript-vault-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PlayScript Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely save, share, and version play scripts on IPFS to preserve authorship and facilitate collaboration. Discipline: Theater & Live Performance (playwriting). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT upload guarantees immutable script versions pinned with unique CIDs for provenance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlayScriptVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPlayScriptVault /// @notice Securely save, share, and version play scripts on IPFS to preserve authorship and facilitate collaboration. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPlayScriptVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlayScriptVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlayScriptVault.sol PlayScriptVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a playwriting, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-playwright-feedback-chain-4 · theater · Playwright Feedback Chain ===== Pitch: Mint feedback and revisions on scripts as NFTs for transparent version control. Page: https://arcprompts.lovable.app/ideas/theater-playwright-feedback-chain-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Playwright Feedback Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint feedback and revisions on scripts as NFTs for transparent version control. Discipline: Theater & Live Performance (script development). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance tracks iterative script changes with verified contributor ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlaywrightFeedbackChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PlaywrightFeedbackChain /// @notice ERC-721 provenance for: Mint feedback and revisions on scripts as NFTs for transparent version control. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PlaywrightFeedbackChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PlaywrightFeedbackChain", "PLAYWR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlaywrightFeedbackChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlaywrightFeedbackChain.sol PlaywrightFeedbackChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a script development, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-playwright-guild-18 · theater · Playwright Guild ===== Pitch: Create tokenized playwright guilds with seamless social login and gasless member transactions. Page: https://arcprompts.lovable.app/ideas/theater-playwright-guild-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Playwright Guild" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create tokenized playwright guilds with seamless social login and gasless member transactions. Discipline: Theater & Live Performance (community membership). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social simplifies membership; sponsored tx enable costless governance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlaywrightGuild.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPlaywrightGuild /// @notice Create tokenized playwright guilds with seamless social login and gasless member transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPlaywrightGuild { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlaywrightGuild` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlaywrightGuild.sol PlaywrightGuild` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every community membership action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-playwright-royalty-nfts-21 · theater · Playwright Royalty NFTs ===== Pitch: Mint royalty shares as NFTs to transparently track playwriting revenue splits. Page: https://arcprompts.lovable.app/ideas/theater-playwright-royalty-nfts-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Playwright Royalty NFTs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint royalty shares as NFTs to transparently track playwriting revenue splits. Discipline: Theater & Live Performance (royalty management). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs allow fractionalized, verifiable royalty ownership and transfers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlaywrightRoyaltyNFTs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PlaywrightRoyaltyNFTs /// @notice ERC-721 provenance for: Mint royalty shares as NFTs to transparently track playwriting revenue splits. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PlaywrightRoyaltyNFTs is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PlaywrightRoyaltyNFTs", "PLAYWR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlaywrightRoyaltyNFTs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlaywrightRoyaltyNFTs.sol PlaywrightRoyaltyNFTs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a royalty management, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-promochain-posters-20 · theater · PromoChain Posters ===== Pitch: Distribute limited-edition promo posters as NFTs with easy social wallet minting and free tx. Page: https://arcprompts.lovable.app/ideas/theater-promochain-posters-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PromoChain Posters" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Distribute limited-edition promo posters as NFTs with easy social wallet minting and free tx. Discipline: Theater & Live Performance (marketing assets). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social verifies fans; sponsored tx remove minting costs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PromoChainPosters.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPromoChainPosters /// @notice Distribute limited-edition promo posters as NFTs with easy social wallet minting and free tx. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPromoChainPosters { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PromoChainPosters` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PromoChainPosters.sol PromoChainPosters` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every marketing assets action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-promptchain-impro-3 · theater · PromptChain Impro ===== Pitch: Generate and track custom improv prompts securely stored and shared via smart contracts. Page: https://arcprompts.lovable.app/ideas/theater-promptchain-impro-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PromptChain Impro" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Generate and track custom improv prompts securely stored and shared via smart contracts. Discipline: Theater & Live Performance (improvisation prompts). Onchain primitive: Arc smart contract. Why this primitive: Sepolia platform ensures provenance and fair sharing of creative prompts in improv communities. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PromptChainImpro.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PromptChainImpro /// @notice Generate and track custom improv prompts securely stored and shared via smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PromptChainImpro { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PromptChainImpro` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PromptChainImpro.sol PromptChainImpro` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a improvisation prompts action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-propblueprint-cache-15 · theater · PropBlueprint Cache ===== Pitch: Store and share detailed prop blueprints and assembly guides pinned immutably on IPFS for artisans. Page: https://arcprompts.lovable.app/ideas/theater-propblueprint-cache-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PropBlueprint Cache" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store and share detailed prop blueprints and assembly guides pinned immutably on IPFS for artisans. Discipline: Theater & Live Performance (prop design). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata uploads ensure permanence and decentralized access to critical prop design files. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PropBlueprintCache.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPropBlueprintCache /// @notice Store and share detailed prop blueprints and assembly guides pinned immutably on IPFS for artisans. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPropBlueprintCache { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PropBlueprintCache` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PropBlueprintCache.sol PropBlueprintCache` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a prop design, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-propchain-ledger-2 · theater · PropChain Ledger ===== Pitch: Track and verify theater prop history and ownership with private social access and free transactions. Page: https://arcprompts.lovable.app/ideas/theater-propchain-ledger-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PropChain Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track and verify theater prop history and ownership with private social access and free transactions. Discipline: Theater & Live Performance (prop provenance). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social secures user access; sponsored tx keeps record updates affordable. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PropChainLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPropChainLedger /// @notice Track and verify theater prop history and ownership with private social access and free transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPropChainLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PropChainLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PropChainLedger.sol PropChainLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every prop provenance action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-propchain-registry-5 · theater · PropChain Registry ===== Pitch: Record and transfer ownership history of stage props transparently on a blockchain registry. Page: https://arcprompts.lovable.app/ideas/theater-propchain-registry-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PropChain Registry" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record and transfer ownership history of stage props transparently on a blockchain registry. Discipline: Theater & Live Performance (prop ownership). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contract provides immutable tracking for valuable or rented props. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PropChainRegistry.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PropChainRegistry /// @notice Record and transfer ownership history of stage props transparently on a blockchain registry. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PropChainRegistry { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PropChainRegistry` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PropChainRegistry.sol PropChainRegistry` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a prop ownership action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-propstoken-swap-21 · theater · PropsToken Swap ===== Pitch: Create a tokenized system for lending and borrowing props between theater companies. Page: https://arcprompts.lovable.app/ideas/theater-propstoken-swap-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PropsToken Swap" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create a tokenized system for lending and borrowing props between theater companies. Discipline: Theater & Live Performance (collaborative prop lending). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts automate lending agreements and track ownership transfers securely. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PropsTokenSwap.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PropsTokenSwap /// @notice Create a tokenized system for lending and borrowing props between theater companies. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PropsTokenSwap { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PropsTokenSwap` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PropsTokenSwap.sol PropsTokenSwap` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a collaborative prop lending action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-rehearsallog-chain-15 · theater · RehearsalLog Chain ===== Pitch: Immutably store rehearsal attendance and notes to improve production accountability. Page: https://arcprompts.lovable.app/ideas/theater-rehearsallog-chain-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "RehearsalLog Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Immutably store rehearsal attendance and notes to improve production accountability. Discipline: Theater & Live Performance (rehearsal tracking). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts lock in rehearsal data to prevent disputes over participation. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/RehearsalLogChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title RehearsalLogChain /// @notice Immutably store rehearsal attendance and notes to improve production accountability. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract RehearsalLogChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs RehearsalLogChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/RehearsalLogChain.sol RehearsalLogChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a rehearsal tracking action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-rehearsalrecord-9 · theater · RehearsalRecord ===== Pitch: Securely archive rehearsal footage and notes with social wallet authentication and free updates. Page: https://arcprompts.lovable.app/ideas/theater-rehearsalrecord-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "RehearsalRecord" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely archive rehearsal footage and notes with social wallet authentication and free updates. Discipline: Theater & Live Performance (performance archives). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social grants creator access; sponsored transactions enable unlimited archival without cost. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/RehearsalRecord.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogRehearsalRecord /// @notice Securely archive rehearsal footage and notes with social wallet authentication and free updates. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogRehearsalRecord { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs RehearsalRecord` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/RehearsalRecord.sol RehearsalRecord` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every performance archives action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-roleauction-platform-1 · theater · RoleAuction Platform ===== Pitch: Decentralized audition bids let performers compete transparently for roles via smart contracts. Page: https://arcprompts.lovable.app/ideas/theater-roleauction-platform-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "RoleAuction Platform" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Decentralized audition bids let performers compete transparently for roles via smart contracts. Discipline: Theater & Live Performance (casting marketplace). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts automate bidding and escrow without intermediaries, enhancing trust in casting. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/RoleAuctionPlatform.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title RoleAuctionPlatform /// @notice Decentralized audition bids let performers compete transparently for roles via smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract RoleAuctionPlatform { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs RoleAuctionPlatform` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/RoleAuctionPlatform.sol RoleAuctionPlatform` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a casting marketplace action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-roleswap-network-11 · theater · RoleSwap Network ===== Pitch: Facilitate secure actor role exchanges and confirmations with social wallets and sponsored transactions. Page: https://arcprompts.lovable.app/ideas/theater-roleswap-network-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "RoleSwap Network" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Facilitate secure actor role exchanges and confirmations with social wallets and sponsored transactions. Discipline: Theater & Live Performance (cast management). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social verifies identities; sponsored tx allow gasless role swaps. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/RoleSwapNetwork.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogRoleSwapNetwork /// @notice Facilitate secure actor role exchanges and confirmations with social wallets and sponsored transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogRoleSwapNetwork { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs RoleSwapNetwork` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/RoleSwapNetwork.sol RoleSwapNetwork` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every cast management action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-scene-script-provenance-0 · theater · Scene Script Provenance ===== Pitch: Securely mint and track original play scripts as creator-owned NFTs. Page: https://arcprompts.lovable.app/ideas/theater-scene-script-provenance-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Scene Script Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely mint and track original play scripts as creator-owned NFTs. Discipline: Theater & Live Performance (playwriting). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Minting scripts as NFTs ensures immutable proof of authorship and timestamps. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SceneScriptProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SceneScriptProvenance /// @notice ERC-721 provenance for: Securely mint and track original play scripts as creator-owned NFTs. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SceneScriptProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SceneScriptProvenance", "SCENES") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SceneScriptProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SceneScriptProvenance.sol SceneScriptProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a playwriting, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-scenechange-nfts-11 · theater · SceneChange NFTs ===== Pitch: Artists mint NFT certificates of original scene designs for resale and licensing. Page: https://arcprompts.lovable.app/ideas/theater-scenechange-nfts-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SceneChange NFTs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Artists mint NFT certificates of original scene designs for resale and licensing. Discipline: Theater & Live Performance (scene ownership). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts prove scene design ownership and facilitate secondary market sales. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SceneChangeNFTs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SceneChangeNFTs /// @notice Artists mint NFT certificates of original scene designs for resale and licensing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SceneChangeNFTs { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SceneChangeNFTs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SceneChangeNFTs.sol SceneChangeNFTs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a scene ownership action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-sceneswap-marketplace-17 · theater · SceneSwap Marketplace ===== Pitch: Trade and license scene and set designs through a verified onchain marketplace. Page: https://arcprompts.lovable.app/ideas/theater-sceneswap-marketplace-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SceneSwap Marketplace" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Trade and license scene and set designs through a verified onchain marketplace. Discipline: Theater & Live Performance (design exchange). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts securely facilitate ownership transfer and licensing payments. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SceneSwapMarketplace.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SceneSwapMarketplace /// @notice Trade and license scene and set designs through a verified onchain marketplace. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SceneSwapMarketplace { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SceneSwapMarketplace` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SceneSwapMarketplace.sol SceneSwapMarketplace` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a design exchange action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-sceneswap-marketplace-5 · theater · SceneSwap Marketplace ===== Pitch: Enable gas-free peer-to-peer trading of set designs via social wallet and sponsored tx. Page: https://arcprompts.lovable.app/ideas/theater-sceneswap-marketplace-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SceneSwap Marketplace" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable gas-free peer-to-peer trading of set designs via social wallet and sponsored tx. Discipline: Theater & Live Performance (set design exchange). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social authenticates users; sponsored transactions enable fee-less trades. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SceneSwapMarketplace.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSceneSwapMarketplace /// @notice Enable gas-free peer-to-peer trading of set designs via social wallet and sponsored tx. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSceneSwapMarketplace { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SceneSwapMarketplace` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SceneSwapMarketplace.sol SceneSwapMarketplace` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every set design exchange action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-script-ownership-registry-24 · theater · Script Ownership Registry ===== Pitch: Mint scripts as NFTs to create an immutable ownership registry for playwrights. Page: https://arcprompts.lovable.app/ideas/theater-script-ownership-registry-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Script Ownership Registry" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint scripts as NFTs to create an immutable ownership registry for playwrights. Discipline: Theater & Live Performance (legal protection). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs provide public, tamper-proof proof of script authorship. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptOwnershipRegistry.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ScriptOwnershipRegistry /// @notice ERC-721 provenance for: Mint scripts as NFTs to create an immutable ownership registry for playwrights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ScriptOwnershipRegistry is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ScriptOwnershipRegistry", "SCRIPT") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptOwnershipRegistry` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptOwnershipRegistry.sol ScriptOwnershipRegistry` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a legal protection, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-script-translation-nfts-14 · theater · Script Translation NFTs ===== Pitch: Mint translated scripts as NFTs to protect translators' intellectual property. Page: https://arcprompts.lovable.app/ideas/theater-script-translation-nfts-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Script Translation NFTs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint translated scripts as NFTs to protect translators' intellectual property. Discipline: Theater & Live Performance (translation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs ensure verified ownership of individual translated script versions. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptTranslationNFTs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ScriptTranslationNFTs /// @notice ERC-721 provenance for: Mint translated scripts as NFTs to protect translators' intellectual property. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ScriptTranslationNFTs is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ScriptTranslationNFTs", "SCRIPT") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptTranslationNFTs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptTranslationNFTs.sol ScriptTranslationNFTs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a translation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-scriptarchive-dao-21 · theater · ScriptArchive DAO ===== Pitch: Preserve scripts onchain with collaborative DAO management and gasless transactions via social wallets. Page: https://arcprompts.lovable.app/ideas/theater-scriptarchive-dao-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptArchive DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Preserve scripts onchain with collaborative DAO management and gasless transactions via social wallets. Discipline: Theater & Live Performance (preservation). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social enables decentralized governance; sponsored tx reduce cost barriers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptArchiveDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogScriptArchiveDAO /// @notice Preserve scripts onchain with collaborative DAO management and gasless transactions via social wallets. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogScriptArchiveDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptArchiveDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptArchiveDAO.sol ScriptArchiveDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every preservation action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-scriptchain-ledger-0 · theater · ScriptChain Ledger ===== Pitch: Securely track and timestamp script drafts through an immutable blockchain ledger. Page: https://arcprompts.lovable.app/ideas/theater-scriptchain-ledger-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptChain Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely track and timestamp script drafts through an immutable blockchain ledger. Discipline: Theater & Live Performance (playwright collaboration). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contract ensures tamper-proof script version control and authorship verification. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptChainLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ScriptChainLedger /// @notice Securely track and timestamp script drafts through an immutable blockchain ledger. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ScriptChainLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptChainLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptChainLedger.sol ScriptChainLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a playwright collaboration action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-scriptlens-dao-16 · theater · ScriptLens DAO ===== Pitch: Enable playwrights and directors to co-own scripts via decentralized governance and revenue sharing. Page: https://arcprompts.lovable.app/ideas/theater-scriptlens-dao-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptLens DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable playwrights and directors to co-own scripts via decentralized governance and revenue sharing. Discipline: Theater & Live Performance (script co-creation). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enable onchain voting and rights management between collaborators. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptLensDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ScriptLensDAO /// @notice Enable playwrights and directors to co-own scripts via decentralized governance and revenue sharing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ScriptLensDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptLensDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptLensDAO.sol ScriptLensDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a script co-creation action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-scriptrevision-chain-7 · theater · ScriptRevision Chain ===== Pitch: Immutable version control for script edits stored and shared pinned on IPFS for playwrights and directors. Page: https://arcprompts.lovable.app/ideas/theater-scriptrevision-chain-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptRevision Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Immutable version control for script edits stored and shared pinned on IPFS for playwrights and directors. Discipline: Theater & Live Performance (script editing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata's JWT upload guarantees tamper-proof, timestamped script versions accessible globally. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptRevisionChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentScriptRevisionChain /// @notice Immutable version control for script edits stored and shared pinned on IPFS for playwrights and directors. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentScriptRevisionChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptRevisionChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptRevisionChain.sol ScriptRevisionChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a script editing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-scriptroyalty-split-6 · theater · ScriptRoyalty Split ===== Pitch: Automate fair payouts to playwrights, directors, and actors using onchain royalty splits. Page: https://arcprompts.lovable.app/ideas/theater-scriptroyalty-split-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptRoyalty Split" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Automate fair payouts to playwrights, directors, and actors using onchain royalty splits. Discipline: Theater & Live Performance (royalty distribution). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts ensure transparent, trustless revenue sharing from live performances. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptRoyaltySplit.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ScriptRoyaltySplit /// @notice Automate fair payouts to playwrights, directors, and actors using onchain royalty splits. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ScriptRoyaltySplit { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptRoyaltySplit` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptRoyaltySplit.sol ScriptRoyaltySplit` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a royalty distribution action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-scriptshare-vault-1 · theater · ScriptShare Vault ===== Pitch: Facilitate secure, collaborative script revisions with onchain identity and no-fee transactions. Page: https://arcprompts.lovable.app/ideas/theater-scriptshare-vault-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptShare Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Facilitate secure, collaborative script revisions with onchain identity and no-fee transactions. Discipline: Theater & Live Performance (script collaboration). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social ensures verified users and sponsored tx enable frictionless edits. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptShareVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogScriptShareVault /// @notice Facilitate secure, collaborative script revisions with onchain identity and no-fee transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogScriptShareVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptShareVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptShareVault.sol ScriptShareVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every script collaboration action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-scripttoken-rights-13 · theater · ScriptToken Rights ===== Pitch: Manage and transfer script rights securely via social wallet and sponsored onchain transactions. Page: https://arcprompts.lovable.app/ideas/theater-scripttoken-rights-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptToken Rights" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Manage and transfer script rights securely via social wallet and sponsored onchain transactions. Discipline: Theater & Live Performance (rights management). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social assures identity; sponsored tx facilitate free rights transfers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptTokenRights.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogScriptTokenRights /// @notice Manage and transfer script rights securely via social wallet and sponsored onchain transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogScriptTokenRights { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptTokenRights` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptTokenRights.sol ScriptTokenRights` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every rights management action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-set-design-archives-3 · theater · Set Design Archives ===== Pitch: Mint 3D set designs as NFTs for secure sharing and historical provenance. Page: https://arcprompts.lovable.app/ideas/theater-set-design-archives-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Set Design Archives" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint 3D set designs as NFTs for secure sharing and historical provenance. Discipline: Theater & Live Performance (scenic design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs authenticate original scenic design assets on an immutable ledger. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SetDesignArchives.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SetDesignArchives /// @notice ERC-721 provenance for: Mint 3D set designs as NFTs for secure sharing and historical provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SetDesignArchives is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SetDesignArchives", "SETDES") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SetDesignArchives` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SetDesignArchives.sol SetDesignArchives` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a scenic design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-set-lighting-automation-nfts-18 · theater · Set Lighting Automation NFTs ===== Pitch: Mint automated lighting sequences as NFTs to secure ownership and resale rights. Page: https://arcprompts.lovable.app/ideas/theater-set-lighting-automation-nfts-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Set Lighting Automation NFTs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint automated lighting sequences as NFTs to secure ownership and resale rights. Discipline: Theater & Live Performance (lighting tech). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs certify original lighting automation and code assets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SetLightingAutomationNFTs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SetLightingAutomationNFTs /// @notice ERC-721 provenance for: Mint automated lighting sequences as NFTs to secure ownership and resale rights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SetLightingAutomationNFTs is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SetLightingAutomationNFTs", "SETLIG") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SetLightingAutomationNFTs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SetLightingAutomationNFTs.sol SetLightingAutomationNFTs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a lighting tech, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-setbuild-contracts-16 · theater · SetBuild Contracts ===== Pitch: Automate set construction contracts with social wallet signing and no-fee onchain transactions. Page: https://arcprompts.lovable.app/ideas/theater-setbuild-contracts-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SetBuild Contracts" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Automate set construction contracts with social wallet signing and no-fee onchain transactions. Discipline: Theater & Live Performance (production agreements). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social ensures contract parties; sponsored tx enable cost-free contract execution. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SetBuildContracts.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSetBuildContracts /// @notice Automate set construction contracts with social wallet signing and no-fee onchain transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSetBuildContracts { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SetBuildContracts` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SetBuildContracts.sol SetBuildContracts` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every production agreements action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-setdesign-manifest-4 · theater · SetDesign Manifest ===== Pitch: Store and share complex 3D set designs and manifests immutably on IPFS for production coordination. Page: https://arcprompts.lovable.app/ideas/theater-setdesign-manifest-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SetDesign Manifest" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store and share complex 3D set designs and manifests immutably on IPFS for production coordination. Discipline: Theater & Live Performance (set design). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata's JWT upload pins large JSON manifests reliably to IPFS for decentralized access. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SetDesignManifest.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentSetDesignManifest /// @notice Store and share complex 3D set designs and manifests immutably on IPFS for production coordination. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentSetDesignManifest { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SetDesignManifest` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SetDesignManifest.sol SetDesignManifest` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a set design, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-setinventory-nfts-24 · theater · SetInventory NFTs ===== Pitch: Tokenize set inventory for easy rental and ownership tracking with social wallets and free tx. Page: https://arcprompts.lovable.app/ideas/theater-setinventory-nfts-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SetInventory NFTs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Tokenize set inventory for easy rental and ownership tracking with social wallets and free tx. Discipline: Theater & Live Performance (asset tokenization). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social authenticates owners; sponsored tx allow gasless NFT transfers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SetInventoryNFTs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSetInventoryNFTs /// @notice Tokenize set inventory for easy rental and ownership tracking with social wallets and free tx. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSetInventoryNFTs { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SetInventoryNFTs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SetInventoryNFTs.sol SetInventoryNFTs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every asset tokenization action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-setlighting-simulator-23 · theater · SetLighting Simulator ===== Pitch: Pin lighting simulation data and images on IPFS for real-time, distributed production previews. Page: https://arcprompts.lovable.app/ideas/theater-setlighting-simulator-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SetLighting Simulator" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin lighting simulation data and images on IPFS for real-time, distributed production previews. Discipline: Theater & Live Performance (previsualization). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s IPFS storage provides permanent hosting for complex simulation assets shared globally. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SetLightingSimulator.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentSetLightingSimulator /// @notice Pin lighting simulation data and images on IPFS for real-time, distributed production previews. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentSetLightingSimulator { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SetLightingSimulator` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SetLightingSimulator.sol SetLightingSimulator` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a previsualization, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-setpiece-provenance-24 · theater · SetPiece Provenance ===== Pitch: Archive and authenticate historical set pieces’ origin and usage on an immutable blockchain record. Page: https://arcprompts.lovable.app/ideas/theater-setpiece-provenance-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SetPiece Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Archive and authenticate historical set pieces’ origin and usage on an immutable blockchain record. Discipline: Theater & Live Performance (historical archival). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide trusted provenance for valuable theatrical artifacts. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SetPieceProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SetPieceProvenance /// @notice Archive and authenticate historical set pieces’ origin and usage on an immutable blockchain record. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SetPieceProvenance { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SetPieceProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SetPieceProvenance.sol SetPieceProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a historical archival action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-setprops-catalog-8 · theater · SetProps Catalog ===== Pitch: Pin and index prop images and descriptions on IPFS for collaborative rental and inventory management. Page: https://arcprompts.lovable.app/ideas/theater-setprops-catalog-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SetProps Catalog" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin and index prop images and descriptions on IPFS for collaborative rental and inventory management. Discipline: Theater & Live Performance (props management). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata's IPFS pinning offers reliable permalinks for decentralized prop catalogs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SetPropsCatalog.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentSetPropsCatalog /// @notice Pin and index prop images and descriptions on IPFS for collaborative rental and inventory management. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentSetPropsCatalog { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SetPropsCatalog` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SetPropsCatalog.sol SetPropsCatalog` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a props management, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-smartstage-access-0 · theater · SmartStage Access ===== Pitch: Enable seamless, gas-free ticketing and VIP experiences via social login and sponsored transactions. Page: https://arcprompts.lovable.app/ideas/theater-smartstage-access-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SmartStage Access" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable seamless, gas-free ticketing and VIP experiences via social login and sponsored transactions. Discipline: Theater & Live Performance (audience engagement). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social and sponsored tx enable hassle-free, onchain ticketing without user gas fees. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SmartStageAccess.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSmartStageAccess /// @notice Enable seamless, gas-free ticketing and VIP experiences via social login and sponsored transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSmartStageAccess { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SmartStageAccess` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SmartStageAccess.sol SmartStageAccess` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every audience engagement action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-soundscape-archive-9 · theater · Soundscape Archive ===== Pitch: Pin ambient and effect sound files with metadata on IPFS to ensure lasting availability and sharing. Page: https://arcprompts.lovable.app/ideas/theater-soundscape-archive-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Soundscape Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin ambient and effect sound files with metadata on IPFS to ensure lasting availability and sharing. Discipline: Theater & Live Performance (sound design). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata supports pinning large audio files and metadata immutably on IPFS for sound designers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SoundscapeArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentSoundscapeArchive /// @notice Pin ambient and effect sound files with metadata on IPFS to ensure lasting availability and sharing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentSoundscapeArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SoundscapeArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SoundscapeArchive.sol SoundscapeArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a sound design, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-soundscape-provenance-8 · theater · Soundscape Provenance ===== Pitch: Mint live performance soundscapes as NFTs to authenticate original audio environments. Page: https://arcprompts.lovable.app/ideas/theater-soundscape-provenance-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Soundscape Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint live performance soundscapes as NFTs to authenticate original audio environments. Discipline: Theater & Live Performance (sound design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs provide immutable timestamps and creator proof for sound designs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SoundscapeProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SoundscapeProvenance /// @notice ERC-721 provenance for: Mint live performance soundscapes as NFTs to authenticate original audio environments. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SoundscapeProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SoundscapeProvenance", "SOUNDS") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SoundscapeProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SoundscapeProvenance.sol SoundscapeProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a sound design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-sponsorspotlight-10 · theater · SponsorSpotlight ===== Pitch: Connect sponsors and shows with onchain contracts signed via social login and no gas fees. Page: https://arcprompts.lovable.app/ideas/theater-sponsorspotlight-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SponsorSpotlight" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Connect sponsors and shows with onchain contracts signed via social login and no gas fees. Discipline: Theater & Live Performance (performance sponsorship). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social enables trust; sponsored tx streamline contract execution without user costs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SponsorSpotlight.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSponsorSpotlight /// @notice Connect sponsors and shows with onchain contracts signed via social login and no gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSponsorSpotlight { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SponsorSpotlight` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SponsorSpotlight.sol SponsorSpotlight` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every performance sponsorship action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-stage-props-provenance-13 · theater · Stage Props Provenance ===== Pitch: Mint digital twins of stage props as NFTs for ownership and rental tracking. Page: https://arcprompts.lovable.app/ideas/theater-stage-props-provenance-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Stage Props Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint digital twins of stage props as NFTs for ownership and rental tracking. Discipline: Theater & Live Performance (prop management). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs track provenance and leasing history of unique stage props. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StagePropsProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title StagePropsProvenance /// @notice ERC-721 provenance for: Mint digital twins of stage props as NFTs for ownership and rental tracking. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StagePropsProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("StagePropsProvenance", "STAGEP") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StagePropsProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StagePropsProvenance.sol StagePropsProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a prop management, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-stagecrew-dao-7 · theater · StageCrew DAO ===== Pitch: Decentralize stage crew decisions and resource allocation through a transparent DAO governed by smart contracts. Page: https://arcprompts.lovable.app/ideas/theater-stagecrew-dao-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StageCrew DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Decentralize stage crew decisions and resource allocation through a transparent DAO governed by smart contracts. Discipline: Theater & Live Performance (crew coordination). Onchain primitive: Arc smart contract. Why this primitive: Sepolia enables trustless voting and fund distribution among live production teams. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StageCrewDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title StageCrewDAO /// @notice Decentralize stage crew decisions and resource allocation through a transparent DAO governed by smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StageCrewDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StageCrewDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StageCrewDAO.sol StageCrewDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a crew coordination action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-stagedesign-dao-12 · theater · StageDesign DAO ===== Pitch: Create decentralized design collectives with gasless voting and social wallet onboarding. Page: https://arcprompts.lovable.app/ideas/theater-stagedesign-dao-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StageDesign DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create decentralized design collectives with gasless voting and social wallet onboarding. Discipline: Theater & Live Performance (design collaboration). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social ensures verified participation; sponsored tx enable costless DAO governance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StageDesignDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogStageDesignDAO /// @notice Create decentralized design collectives with gasless voting and social wallet onboarding. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogStageDesignDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StageDesignDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StageDesignDAO.sol StageDesignDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every design collaboration action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-stagelight-archive-0 · theater · StageLight Archive ===== Pitch: Pin and access dynamic stage lighting setups with permanent IPFS storage for reliable reuse and sharing. Page: https://arcprompts.lovable.app/ideas/theater-stagelight-archive-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StageLight Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin and access dynamic stage lighting setups with permanent IPFS storage for reliable reuse and sharing. Discipline: Theater & Live Performance (lighting design). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata's IPFS ensures lighting designs remain immutable and globally accessible for production teams. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StageLightArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentStageLightArchive /// @notice Pin and access dynamic stage lighting setups with permanent IPFS storage for reliable reuse and sharing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentStageLightArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StageLightArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StageLightArchive.sol StageLightArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a lighting design, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-stagelight-token-2 · theater · StageLight Token ===== Pitch: Issue and trade usage rights for lighting designs using onchain tokens to protect creators. Page: https://arcprompts.lovable.app/ideas/theater-stagelight-token-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StageLight Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Issue and trade usage rights for lighting designs using onchain tokens to protect creators. Discipline: Theater & Live Performance (lighting rights management). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts enable verifiable licensing and royalty distribution for lighting designers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StageLightToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title StageLightToken /// @notice Issue and trade usage rights for lighting designs using onchain tokens to protect creators. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StageLightToken { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StageLightToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StageLightToken.sol StageLightToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a lighting rights management action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-stagesafety-logs-21 · theater · StageSafety Logs ===== Pitch: Pin safety inspection reports and compliance logs on IPFS ensuring tamper-proof records for venues and crews. Page: https://arcprompts.lovable.app/ideas/theater-stagesafety-logs-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StageSafety Logs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin safety inspection reports and compliance logs on IPFS ensuring tamper-proof records for venues and crews. Discipline: Theater & Live Performance (safety compliance). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata guarantees permanent, tamper-resistant storage of critical safety documentation on IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StageSafetyLogs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentStageSafetyLogs /// @notice Pin safety inspection reports and compliance logs on IPFS ensuring tamper-proof records for venues and crews. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentStageSafetyLogs { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StageSafetyLogs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StageSafetyLogs.sol StageSafetyLogs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a safety compliance, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-stagesound-dao-22 · theater · StageSound DAO ===== Pitch: Coordinate and monetize collective sound design projects with transparent DAOs and revenue splits. Page: https://arcprompts.lovable.app/ideas/theater-stagesound-dao-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StageSound DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Coordinate and monetize collective sound design projects with transparent DAOs and revenue splits. Discipline: Theater & Live Performance (sound design collaboration). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enable trustless profit sharing and collaborative decision-making. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StageSoundDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title StageSoundDAO /// @notice Coordinate and monetize collective sound design projects with transparent DAOs and revenue splits. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StageSoundDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StageSoundDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StageSoundDAO.sol StageSoundDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a sound design collaboration action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-tickettrust-nft-4 · theater · TicketTrust NFT ===== Pitch: Use blockchain to mint tamper-proof NFT tickets preventing counterfeiting for live performances. Page: https://arcprompts.lovable.app/ideas/theater-tickettrust-nft-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "TicketTrust NFT" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Use blockchain to mint tamper-proof NFT tickets preventing counterfeiting for live performances. Discipline: Theater & Live Performance (ticket authenticity). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide secure, verifiable ownership records for live event tickets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TicketTrustNFT.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title TicketTrustNFT /// @notice Use blockchain to mint tamper-proof NFT tickets preventing counterfeiting for live performances. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TicketTrustNFT { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TicketTrustNFT` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TicketTrustNFT.sol TicketTrustNFT` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a ticket authenticity action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-venuemap-index-12 · theater · VenueMap Index ===== Pitch: Pin detailed venue and stage layout manifests on IPFS for directors and designers to coordinate setups. Page: https://arcprompts.lovable.app/ideas/theater-venuemap-index-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VenueMap Index" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin detailed venue and stage layout manifests on IPFS for directors and designers to coordinate setups. Discipline: Theater & Live Performance (stage layout). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata supports pinning detailed JSON layout descriptors accessible for all production stakeholders. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VenueMapIndex.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentVenueMapIndex /// @notice Pin detailed venue and stage layout manifests on IPFS for directors and designers to coordinate setups. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentVenueMapIndex { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VenueMapIndex` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VenueMapIndex.sol VenueMapIndex` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a stage layout, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-virtual-backstage-pass-16 · theater · Virtual Backstage Pass ===== Pitch: Issue NFT backstage passes granting exclusive digital and live experience. Page: https://arcprompts.lovable.app/ideas/theater-virtual-backstage-pass-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Virtual Backstage Pass" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Issue NFT backstage passes granting exclusive digital and live experience. Discipline: Theater & Live Performance (fan engagement). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT passes confirm exclusive access and prevent counterfeit entries. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VirtualBackstagePass.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title VirtualBackstagePass /// @notice ERC-721 provenance for: Issue NFT backstage passes granting exclusive digital and live experience. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VirtualBackstagePass is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("VirtualBackstagePass", "VIRTUA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VirtualBackstagePass` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VirtualBackstagePass.sol VirtualBackstagePass` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a fan engagement, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-virtual-set-nfts-23 · theater · Virtual Set NFTs ===== Pitch: Mint virtual set designs as NFTs for use in augmented and virtual performances. Page: https://arcprompts.lovable.app/ideas/theater-virtual-set-nfts-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Virtual Set NFTs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint virtual set designs as NFTs for use in augmented and virtual performances. Discipline: Theater & Live Performance (digital scenography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs authenticate digital stage assets for cross-platform use. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VirtualSetNFTs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title VirtualSetNFTs /// @notice ERC-721 provenance for: Mint virtual set designs as NFTs for use in augmented and virtual performances. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VirtualSetNFTs is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("VirtualSetNFTs", "VIRTUA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VirtualSetNFTs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VirtualSetNFTs.sol VirtualSetNFTs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a digital scenography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-virtualstage-access-22 · theater · VirtualStage Access ===== Pitch: Grant gasless access to hybrid live/virtual shows via social wallet login and sponsored tx. Page: https://arcprompts.lovable.app/ideas/theater-virtualstage-access-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VirtualStage Access" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Grant gasless access to hybrid live/virtual shows via social wallet login and sponsored tx. Discipline: Theater & Live Performance (hybrid performances). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social simplifies ticketing; sponsored tx enable seamless access without gas. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VirtualStageAccess.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogVirtualStageAccess /// @notice Grant gasless access to hybrid live/virtual shows via social wallet login and sponsored tx. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogVirtualStageAccess { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VirtualStageAccess` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VirtualStageAccess.sol VirtualStageAccess` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every hybrid performances action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA theater-virtualstage-seeds-18 · theater · VirtualStage Seeds ===== Pitch: Pin virtual stage assets and scene manifests on IPFS for seamless integration in live and hybrid performances. Page: https://arcprompts.lovable.app/ideas/theater-virtualstage-seeds-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VirtualStage Seeds" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin virtual stage assets and scene manifests on IPFS for seamless integration in live and hybrid performances. Discipline: Theater & Live Performance (virtual set design). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata's IPFS pinning provides reliable decentralized storage for virtual stage asset distribution. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VirtualStageSeeds.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentVirtualStageSeeds /// @notice Pin virtual stage assets and scene manifests on IPFS for seamless integration in live and hybrid performances. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentVirtualStageSeeds { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VirtualStageSeeds` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VirtualStageSeeds.sol VirtualStageSeeds` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a virtual set design, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-360proof-token-24 · video · 360Proof Token ===== Pitch: Secure authenticity of 360° videos by minting creator-owned provenance NFTs. Page: https://arcprompts.lovable.app/ideas/video-360proof-token-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "360Proof Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Secure authenticity of 360° videos by minting creator-owned provenance NFTs. Discipline: Videography & Film (360-degree videography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 mints IPFS 360° video data proving ownership and originality. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/360ProofToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title 360ProofToken /// @notice ERC-721 provenance for: Secure authenticity of 360° videos by minting creator-owned provenance NFTs. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract 360ProofToken is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("360ProofToken", "360PRO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs 360ProofToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/360ProofToken.sol 360ProofToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a 360-degree videography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-assetaudit-trail-13 · video · AssetAudit Trail ===== Pitch: Pin asset usage logs and metadata on IPFS to audit rights and permissions transparently. Page: https://arcprompts.lovable.app/ideas/video-assetaudit-trail-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AssetAudit Trail" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin asset usage logs and metadata on IPFS to audit rights and permissions transparently. Discipline: Videography & Film (media asset management). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata immutably stores JSON manifests of asset provenance, securing usage compliance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AssetAuditTrail.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAssetAuditTrail /// @notice Pin asset usage logs and metadata on IPFS to audit rights and permissions transparently. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAssetAuditTrail { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AssetAuditTrail` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AssetAuditTrail.sol AssetAuditTrail` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a media asset management, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-autosubtitle-mint-11 · video · AutoSubtitle Mint ===== Pitch: Generate and store verified subtitles onchain to guarantee transcript authenticity. Page: https://arcprompts.lovable.app/ideas/video-autosubtitle-mint-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AutoSubtitle Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Generate and store verified subtitles onchain to guarantee transcript authenticity. Discipline: Videography & Film (subtitle generation). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts timestamp and verify subtitle data immutably on Sepolia network. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AutoSubtitleMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AutoSubtitleMint /// @notice Generate and store verified subtitles onchain to guarantee transcript authenticity. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AutoSubtitleMint { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AutoSubtitleMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AutoSubtitleMint.sol AutoSubtitleMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a subtitle generation action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-captioncast-ipfs-8 · video · CaptionCast IPFS ===== Pitch: Store and distribute subtitle files on IPFS for reliable, versioned caption accessibility worldwide. Page: https://arcprompts.lovable.app/ideas/video-captioncast-ipfs-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "CaptionCast IPFS" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store and distribute subtitle files on IPFS for reliable, versioned caption accessibility worldwide. Discipline: Videography & Film (subtitling and captions). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s IPFS pinning ensures subtitles are immutable, accessible, and easily shared across platforms. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CaptionCastIPFS.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentCaptionCastIPFS /// @notice Store and distribute subtitle files on IPFS for reliable, versioned caption accessibility worldwide. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentCaptionCastIPFS { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CaptionCastIPFS` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CaptionCastIPFS.sol CaptionCastIPFS` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a subtitling and captions, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-captiontag-nft-19 · video · CaptionTag NFT ===== Pitch: Mint caption sets as NFTs to prove creative ownership and enable resale. Page: https://arcprompts.lovable.app/ideas/video-captiontag-nft-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "CaptionTag NFT" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint caption sets as NFTs to prove creative ownership and enable resale. Discipline: Videography & Film (caption ownership). Onchain primitive: Arc smart contract. Why this primitive: Sepolia NFTs securely represent unique caption data and ownership rights. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CaptionTagNFT.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title CaptionTagNFT /// @notice Mint caption sets as NFTs to prove creative ownership and enable resale. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CaptionTagNFT { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CaptionTagNFT` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CaptionTagNFT.sol CaptionTagNFT` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a caption ownership action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-chain-backed-storyboards-11 · video · Chain-Backed Storyboards ===== Pitch: Store and share editable storyboards onchain with instant, gas-free updates. Page: https://arcprompts.lovable.app/ideas/video-chain-backed-storyboards-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Chain-Backed Storyboards" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store and share editable storyboards onchain with instant, gas-free updates. Discipline: Videography & Film (previsualization). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored tx allow real-time storyboard collaboration without gas user barrier. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChainBackedStoryboards.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogChainBackedStoryboards /// @notice Store and share editable storyboards onchain with instant, gas-free updates. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogChainBackedStoryboards { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChainBackedStoryboards` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChainBackedStoryboards.sol ChainBackedStoryboards` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every previsualization action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-chain-linked-shot-lists-21 · video · Chain-Linked Shot Lists ===== Pitch: Manage shot lists collaboratively onchain without gas fees for production teams. Page: https://arcprompts.lovable.app/ideas/video-chain-linked-shot-lists-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Chain-Linked Shot Lists" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Manage shot lists collaboratively onchain without gas fees for production teams. Discipline: Videography & Film (production planning). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy's gasless tx aid real-time, user-friendly shot list updates. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChainLinkedShotLists.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogChainLinkedShotLists /// @notice Manage shot lists collaboratively onchain without gas fees for production teams. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogChainLinkedShotLists { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChainLinkedShotLists` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChainLinkedShotLists.sol ChainLinkedShotLists` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every production planning action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-chain-stamped-edits-16 · video · Chain-Stamped Edits ===== Pitch: Stamp finalized edits onchain instantly to prove authenticity without gas hassles. Page: https://arcprompts.lovable.app/ideas/video-chain-stamped-edits-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Chain-Stamped Edits" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Stamp finalized edits onchain instantly to prove authenticity without gas hassles. Discipline: Videography & Film (edit authenticity). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy sponsored tx ensure cost-free, verifiable edit timestamping. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChainStampedEdits.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogChainStampedEdits /// @notice Stamp finalized edits onchain instantly to prove authenticity without gas hassles. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogChainStampedEdits { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChainStampedEdits` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChainStampedEdits.sol ChainStampedEdits` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every edit authenticity action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-chainfeedback-loop-14 · video · ChainFeedback Loop ===== Pitch: Collect and verify editorial feedback on video edits onchain to improve collaboration. Page: https://arcprompts.lovable.app/ideas/video-chainfeedback-loop-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ChainFeedback Loop" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Collect and verify editorial feedback on video edits onchain to improve collaboration. Discipline: Videography & Film (editorial feedback). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide permanent, auditable records of feedback and approvals. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ChainFeedbackLoop.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ChainFeedbackLoop /// @notice Collect and verify editorial feedback on video edits onchain to improve collaboration. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ChainFeedbackLoop { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ChainFeedbackLoop` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ChainFeedbackLoop.sol ChainFeedbackLoop` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a editorial feedback action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-clip-provenance-ledger-2 · video · Clip Provenance Ledger ===== Pitch: Verify original footage sources with onchain provenance that users access gaslessly. Page: https://arcprompts.lovable.app/ideas/video-clip-provenance-ledger-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Clip Provenance Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Verify original footage sources with onchain provenance that users access gaslessly. Discipline: Videography & Film (authenticity tracking). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet with Google sign-in simplifies provenance verification with no gas friction. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ClipProvenanceLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogClipProvenanceLedger /// @notice Verify original footage sources with onchain provenance that users access gaslessly. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogClipProvenanceLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ClipProvenanceLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ClipProvenanceLedger.sol ClipProvenanceLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every authenticity tracking action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-clipchain-marketplace-15 · video · ClipChain Marketplace ===== Pitch: Create decentralized marketplaces for licensed video clips pinned immutably on IPFS. Page: https://arcprompts.lovable.app/ideas/video-clipchain-marketplace-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ClipChain Marketplace" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create decentralized marketplaces for licensed video clips pinned immutably on IPFS. Discipline: Videography & Film (clip licensing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s permanent pinning suits immutable clip metadata storage for transparent licensing. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ClipChainMarketplace.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentClipChainMarketplace /// @notice Create decentralized marketplaces for licensed video clips pinned immutably on IPFS. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentClipChainMarketplace { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ClipChainMarketplace` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ClipChainMarketplace.sol ClipChainMarketplace` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a clip licensing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-clipchain-remix-2 · video · ClipChain Remix ===== Pitch: Mint provenance tokens for remixed clips to credit original and derivative creators fairly. Page: https://arcprompts.lovable.app/ideas/video-clipchain-remix-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ClipChain Remix" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint provenance tokens for remixed clips to credit original and derivative creators fairly. Discipline: Videography & Film (video remixing). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 NFT minting verifies remix lineage and enforces creator ownership onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ClipChainRemix.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ClipChainRemix /// @notice ERC-721 provenance for: Mint provenance tokens for remixed clips to credit original and derivative creators fairly. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ClipChainRemix is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ClipChainRemix", "CLIPCH") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ClipChainRemix` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ClipChainRemix.sol ClipChainRemix` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a video remixing, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-cliplicense-swap-2 · video · ClipLicense Swap ===== Pitch: Facilitate secure, onchain licensing and transfer of video clips between creators. Page: https://arcprompts.lovable.app/ideas/video-cliplicense-swap-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ClipLicense Swap" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Facilitate secure, onchain licensing and transfer of video clips between creators. Discipline: Videography & Film (licensed assets). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts automate licensing terms transparently and enforce payments securely. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ClipLicenseSwap.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ClipLicenseSwap /// @notice Facilitate secure, onchain licensing and transfer of video clips between creators. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ClipLicenseSwap { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ClipLicenseSwap` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ClipLicenseSwap.sol ClipLicenseSwap` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a licensed assets action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-clipmint-archival-6 · video · ClipMint Archival ===== Pitch: Mint archival footage as NFTs to certify origin and enable monetization of historic clips. Page: https://arcprompts.lovable.app/ideas/video-clipmint-archival-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ClipMint Archival" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint archival footage as NFTs to certify origin and enable monetization of historic clips. Discipline: Videography & Film (archival footage). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance ensures authenticity and immutable proof of archival IPFS-hosted clips. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ClipMintArchival.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ClipMintArchival /// @notice ERC-721 provenance for: Mint archival footage as NFTs to certify origin and enable monetization of historic clips. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ClipMintArchival is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ClipMintArchival", "CLIPMI") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ClipMintArchival` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ClipMintArchival.sol ClipMintArchival` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a archival footage, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-clipproof-archive-17 · video · ClipProof Archive ===== Pitch: Archive important video clips immutably onchain for indisputable provenance and retrieval. Page: https://arcprompts.lovable.app/ideas/video-clipproof-archive-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ClipProof Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Archive important video clips immutably onchain for indisputable provenance and retrieval. Discipline: Videography & Film (immutable clip storage). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts guarantee permanent, tamper-proof clip hash storage. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ClipProofArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ClipProofArchive /// @notice Archive important video clips immutably onchain for indisputable provenance and retrieval. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ClipProofArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ClipProofArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ClipProofArchive.sol ClipProofArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a immutable clip storage action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-clipstake-platform-10 · video · ClipStake Platform ===== Pitch: Stake tokens on promising clips to support creators and earn rewards via smart contracts. Page: https://arcprompts.lovable.app/ideas/video-clipstake-platform-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ClipStake Platform" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Stake tokens on promising clips to support creators and earn rewards via smart contracts. Discipline: Videography & Film (video staking). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts automate staking, rewards, and clip performance tracking transparently. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ClipStakePlatform.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ClipStakePlatform /// @notice Stake tokens on promising clips to support creators and earn rewards via smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ClipStakePlatform { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ClipStakePlatform` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ClipStakePlatform.sol ClipStakePlatform` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a video staking action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-clipstamp-vault-1 · video · ClipStamp Vault ===== Pitch: Securely timestamp video clips onchain to verify original creation times and prevent plagiarism. Page: https://arcprompts.lovable.app/ideas/video-clipstamp-vault-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ClipStamp Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely timestamp video clips onchain to verify original creation times and prevent plagiarism. Discipline: Videography & Film (content authentication). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata hashes and pins clip metadata on IPFS, providing a decentralized proof of authenticity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ClipStampVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentClipStampVault /// @notice Securely timestamp video clips onchain to verify original creation times and prevent plagiarism. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentClipStampVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ClipStampVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ClipStampVault.sol ClipStampVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a content authentication, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-cliptip-payments-22 · video · ClipTip Payments ===== Pitch: Enable instant micro-tipping to video creators with onchain transparency and low fees. Page: https://arcprompts.lovable.app/ideas/video-cliptip-payments-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ClipTip Payments" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable instant micro-tipping to video creators with onchain transparency and low fees. Discipline: Videography & Film (micro-tipping). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts automate trustless, low-cost tipping transactions. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ClipTipPayments.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ClipTipPayments /// @notice Enable instant micro-tipping to video creators with onchain transparency and low fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ClipTipPayments { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ClipTipPayments` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ClipTipPayments.sol ClipTipPayments` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a micro-tipping action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-collaborative-scene-ledger-6 · video · Collaborative Scene Ledger ===== Pitch: Track scene contributions by multiple creators transparently with zero gas cost onchain. Page: https://arcprompts.lovable.app/ideas/video-collaborative-scene-ledger-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Collaborative Scene Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track scene contributions by multiple creators transparently with zero gas cost onchain. Discipline: Videography & Film (scene co-creation). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy’s gasless sponsored transactions ensure seamless multi-user scene input recording. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CollaborativeSceneLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogCollaborativeSceneLedger /// @notice Track scene contributions by multiple creators transparently with zero gas cost onchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogCollaborativeSceneLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CollaborativeSceneLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CollaborativeSceneLedger.sol CollaborativeSceneLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every scene co-creation action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-colorgrade-ledger-1 · video · ColorGrade Ledger ===== Pitch: Track color correction versions with creator-owned tokens to maintain authentic grading history. Page: https://arcprompts.lovable.app/ideas/video-colorgrade-ledger-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ColorGrade Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track color correction versions with creator-owned tokens to maintain authentic grading history. Discipline: Videography & Film (color grading). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance records immutable color grade IPFS files, ensuring transparent visual edits. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ColorGradeLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ColorGradeLedger /// @notice ERC-721 provenance for: Track color correction versions with creator-owned tokens to maintain authentic grading history. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ColorGradeLedger is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ColorGradeLedger", "COLORG") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ColorGradeLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ColorGradeLedger.sol ColorGradeLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a color grading, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-colorgrade-nft-3 · video · ColorGrade NFT ===== Pitch: Create and trade unique onchain color grade presets as NFTs for creative reuse. Page: https://arcprompts.lovable.app/ideas/video-colorgrade-nft-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ColorGrade NFT" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create and trade unique onchain color grade presets as NFTs for creative reuse. Discipline: Videography & Film (color grading). Onchain primitive: Arc smart contract. Why this primitive: Sepolia NFTs uniquely identify color presets, enabling verified ownership and trade. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ColorGradeNFT.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ColorGradeNFT /// @notice Create and trade unique onchain color grade presets as NFTs for creative reuse. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ColorGradeNFT { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ColorGradeNFT` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ColorGradeNFT.sol ColorGradeNFT` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a color grading action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-colorgrade-provenance-3 · video · ColorGrade Provenance ===== Pitch: Track and share editable color grading profiles via IPFS for collaborative and consistent grading. Page: https://arcprompts.lovable.app/ideas/video-colorgrade-provenance-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ColorGrade Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track and share editable color grading profiles via IPFS for collaborative and consistent grading. Discipline: Videography & Film (color grading). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s IPFS pins store immutable color LUTs and metadata accessible by all stakeholders. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ColorGradeProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentColorGradeProvenance /// @notice Track and share editable color grading profiles via IPFS for collaborative and consistent grading. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentColorGradeProvenance { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ColorGradeProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ColorGradeProvenance.sol ColorGradeProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a color grading, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-colorswap-market-16 · video · ColorSwap Market ===== Pitch: Trade and license unique color palettes onchain for licensed use in video projects. Page: https://arcprompts.lovable.app/ideas/video-colorswap-market-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ColorSwap Market" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Trade and license unique color palettes onchain for licensed use in video projects. Discipline: Videography & Film (color palette trading). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts track ownership and licensing of color palettes as digital assets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ColorSwapMarket.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ColorSwapMarket /// @notice Trade and license unique color palettes onchain for licensed use in video projects. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ColorSwapMarket { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ColorSwapMarket` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ColorSwapMarket.sol ColorSwapMarket` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a color palette trading action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-contenttrust-badge-6 · video · ContentTrust Badge ===== Pitch: Award verifiable trust badges onchain for authentic and verified video content. Page: https://arcprompts.lovable.app/ideas/video-contenttrust-badge-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ContentTrust Badge" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Award verifiable trust badges onchain for authentic and verified video content. Discipline: Videography & Film (video authenticity). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts issue tamper-proof badges proving content legitimacy and origin. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ContentTrustBadge.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ContentTrustBadge /// @notice Award verifiable trust badges onchain for authentic and verified video content. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ContentTrustBadge { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ContentTrustBadge` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ContentTrustBadge.sol ContentTrustBadge` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a video authenticity action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-cutlist-ledger-6 · video · CutList Ledger ===== Pitch: Pin and track cut decisions and timelines on IPFS for transparent editing history and audit trails. Page: https://arcprompts.lovable.app/ideas/video-cutlist-ledger-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "CutList Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin and track cut decisions and timelines on IPFS for transparent editing history and audit trails. Discipline: Videography & Film (editing workflows). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata guarantees immutable storage of timeline JSON manifests, improving workflow accountability. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CutListLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentCutListLedger /// @notice Pin and track cut decisions and timelines on IPFS for transparent editing history and audit trails. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentCutListLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CutListLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CutListLedger.sol CutListLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a editing workflows, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-cutsequence-proof-12 · video · CutSequence Proof ===== Pitch: Secure entire editing sequences as NFTs to confirm edit ownership and version control. Page: https://arcprompts.lovable.app/ideas/video-cutsequence-proof-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "CutSequence Proof" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Secure entire editing sequences as NFTs to confirm edit ownership and version control. Discipline: Videography & Film (editing sequence). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens contain IPFS CIDs of sequences, providing transparent edit authenticity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CutSequenceProof.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title CutSequenceProof /// @notice ERC-721 provenance for: Secure entire editing sequences as NFTs to confirm edit ownership and version control. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CutSequenceProof is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("CutSequenceProof", "CUTSEQ") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CutSequenceProof` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CutSequenceProof.sol CutSequenceProof` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a editing sequence, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-dronefootage-token-20 · video · DroneFootage Token ===== Pitch: Mint drone videos as provenance NFTs to secure unique aerial shot copyrights. Page: https://arcprompts.lovable.app/ideas/video-dronefootage-token-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "DroneFootage Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint drone videos as provenance NFTs to secure unique aerial shot copyrights. Discipline: Videography & Film (aerial videography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens verify IPFS-hosted drone footage authenticity on Sepolia. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DroneFootageToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title DroneFootageToken /// @notice ERC-721 provenance for: Mint drone videos as provenance NFTs to secure unique aerial shot copyrights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DroneFootageToken is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("DroneFootageToken", "DRONEF") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DroneFootageToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DroneFootageToken.sol DroneFootageToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a aerial videography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-edittrace-ledger-5 · video · EditTrace Ledger ===== Pitch: Track and mint every edit step as NFTs to maintain transparent video edit workflows. Page: https://arcprompts.lovable.app/ideas/video-edittrace-ledger-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "EditTrace Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track and mint every edit step as NFTs to maintain transparent video edit workflows. Discipline: Videography & Film (editing history). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain tokenization captures immutable edit provenance with detailed IPFS metadata. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EditTraceLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title EditTraceLedger /// @notice ERC-721 provenance for: Track and mint every edit step as NFTs to maintain transparent video edit workflows. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract EditTraceLedger is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("EditTraceLedger", "EDITTR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EditTraceLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EditTraceLedger.sol EditTraceLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a editing history, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-editversion-ledger-1 · video · EditVersion Ledger ===== Pitch: Track and verify every edit version of a video project transparently on the blockchain. Page: https://arcprompts.lovable.app/ideas/video-editversion-ledger-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "EditVersion Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track and verify every edit version of a video project transparently on the blockchain. Discipline: Videography & Film (edit history). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide immutable version history ensuring transparent edit provenance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EditVersionLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title EditVersionLedger /// @notice Track and verify every edit version of a video project transparently on the blockchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract EditVersionLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EditVersionLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EditVersionLedger.sol EditVersionLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a edit history action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-effectchain-library-18 · video · EffectChain Library ===== Pitch: Pin and share effect preset libraries on IPFS for repeatable, consistent video stylizations. Page: https://arcprompts.lovable.app/ideas/video-effectchain-library-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "EffectChain Library" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin and share effect preset libraries on IPFS for repeatable, consistent video stylizations. Discipline: Videography & Film (effect presets). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata stores immutable effect preset manifests easily distributed to creators. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EffectChainLibrary.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentEffectChainLibrary /// @notice Pin and share effect preset libraries on IPFS for repeatable, consistent video stylizations. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentEffectChainLibrary { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EffectChainLibrary` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EffectChainLibrary.sol EffectChainLibrary` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a effect presets, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-eventsync-ledger-20 · video · EventSync Ledger ===== Pitch: Pin and synchronize live edit events on IPFS for verifiable broadcast and post-event review. Page: https://arcprompts.lovable.app/ideas/video-eventsync-ledger-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "EventSync Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin and synchronize live edit events on IPFS for verifiable broadcast and post-event review. Discipline: Videography & Film (live editing events). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata immutable logs of event JSON manifests ensure tamper-proof edit event histories. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EventSyncLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentEventSyncLedger /// @notice Pin and synchronize live edit events on IPFS for verifiable broadcast and post-event review. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentEventSyncLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EventSyncLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EventSyncLedger.sol EventSyncLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a live editing events, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-filterforge-ipfs-14 · video · FilterForge IPFS ===== Pitch: Share and version custom video filters on IPFS for easy collaboration and reuse. Page: https://arcprompts.lovable.app/ideas/video-filterforge-ipfs-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FilterForge IPFS" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Share and version custom video filters on IPFS for easy collaboration and reuse. Discipline: Videography & Film (filter development). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata pins filter JSON manifests ensuring permanent availability and provenance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FilterForgeIPFS.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentFilterForgeIPFS /// @notice Share and version custom video filters on IPFS for easy collaboration and reuse. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentFilterForgeIPFS { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FilterForgeIPFS` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FilterForgeIPFS.sol FilterForgeIPFS` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a filter development, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-filterforge-token-13 · video · FilterForge Token ===== Pitch: Mint original video filter presets as NFTs to protect and monetize unique creative effects. Page: https://arcprompts.lovable.app/ideas/video-filterforge-token-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FilterForge Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint original video filter presets as NFTs to protect and monetize unique creative effects. Discipline: Videography & Film (video filters). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance links IPFS-hosted filter data with creator identity onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FilterForgeToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title FilterForgeToken /// @notice ERC-721 provenance for: Mint original video filter presets as NFTs to protect and monetize unique creative effects. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FilterForgeToken is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("FilterForgeToken", "FILTER") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FilterForgeToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FilterForgeToken.sol FilterForgeToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a video filters, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-framechain-auth-0 · video · FrameChain Auth ===== Pitch: Verify and timestamp original video frames onchain to prove authentic content ownership instantly. Page: https://arcprompts.lovable.app/ideas/video-framechain-auth-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FrameChain Auth" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Verify and timestamp original video frames onchain to prove authentic content ownership instantly. Discipline: Videography & Film (video copyright). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts securely record and timestamp proofs of originality immutably. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FrameChainAuth.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title FrameChainAuth /// @notice Verify and timestamp original video frames onchain to prove authentic content ownership instantly. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FrameChainAuth { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FrameChainAuth` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FrameChainAuth.sol FrameChainAuth` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a video copyright action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-frameforge-ipfs-17 · video · FrameForge IPFS ===== Pitch: Attach and store notes and metadata to individual frames on IPFS for detailed editorial feedback. Page: https://arcprompts.lovable.app/ideas/video-frameforge-ipfs-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FrameForge IPFS" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Attach and store notes and metadata to individual frames on IPFS for detailed editorial feedback. Discipline: Videography & Film (frame-specific notes). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata pins JSON frame annotations immutably, enabling collaborative frame-level feedback. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FrameForgeIPFS.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentFrameForgeIPFS /// @notice Attach and store notes and metadata to individual frames on IPFS for detailed editorial feedback. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentFrameForgeIPFS { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FrameForgeIPFS` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FrameForgeIPFS.sol FrameForgeIPFS` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a frame-specific notes, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-framelock-provenance-0 · video · FrameLock Provenance ===== Pitch: Securely prove original shot ownership for video editors and creators to prevent unauthorized reuse. Page: https://arcprompts.lovable.app/ideas/video-framelock-provenance-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FrameLock Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely prove original shot ownership for video editors and creators to prevent unauthorized reuse. Discipline: Videography & Film (shot authentication). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance minting uniquely timestamps and attributes original video frames onchain for irrefutable proof. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FrameLockProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title FrameLockProvenance /// @notice ERC-721 provenance for: Securely prove original shot ownership for video editors and creators to prevent unauthorized reuse. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FrameLockProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("FrameLockProvenance", "FRAMEL") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FrameLockProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FrameLockProvenance.sol FrameLockProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a shot authentication, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-framerate-oracle-20 · video · FrameRate Oracle ===== Pitch: Use onchain oracles to validate frame rate data ensuring consistency across platforms. Page: https://arcprompts.lovable.app/ideas/video-framerate-oracle-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FrameRate Oracle" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Use onchain oracles to validate frame rate data ensuring consistency across platforms. Discipline: Videography & Film (metadata validation). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts integrate with oracles for real-time metadata verification. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FrameRateOracle.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title FrameRateOracle /// @notice Use onchain oracles to validate frame rate data ensuring consistency across platforms. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FrameRateOracle { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FrameRateOracle` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FrameRateOracle.sol FrameRateOracle` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a metadata validation action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-framerate-token-4 · video · FrameRate Token ===== Pitch: Store and share verified video metadata like frame rate and resolution securely onchain. Page: https://arcprompts.lovable.app/ideas/video-framerate-token-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FrameRate Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store and share verified video metadata like frame rate and resolution securely onchain. Discipline: Videography & Film (video metadata). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts guarantee immutable, verifiable metadata storage linked to content. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FrameRateToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title FrameRateToken /// @notice Store and share verified video metadata like frame rate and resolution securely onchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FrameRateToken { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FrameRateToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FrameRateToken.sol FrameRateToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a video metadata action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-framesync-ledger-0 · video · FrameSync Ledger ===== Pitch: Track and manage video edits through IPFS to ensure lossless version history for creators. Page: https://arcprompts.lovable.app/ideas/video-framesync-ledger-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FrameSync Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track and manage video edits through IPFS to ensure lossless version history for creators. Discipline: Videography & Film (version control). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS provides immutable storage for every edit's metadata, guaranteeing tamper-proof version control. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FrameSyncLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentFrameSyncLedger /// @notice Track and manage video edits through IPFS to ensure lossless version history for creators. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentFrameSyncLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FrameSyncLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FrameSyncLedger.sol FrameSyncLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a version control, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-gasless-asset-sync-17 · video · Gasless Asset Sync ===== Pitch: Sync media assets across devices and collaborators onchain with no gas fees. Page: https://arcprompts.lovable.app/ideas/video-gasless-asset-sync-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Asset Sync" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Sync media assets across devices and collaborators onchain with no gas fees. Discipline: Videography & Film (media syncing). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets with gasless transactions remove syncing friction for users. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessAssetSync.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessAssetSync /// @notice Sync media assets across devices and collaborators onchain with no gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessAssetSync { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessAssetSync` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessAssetSync.sol GaslessAssetSync` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every media syncing action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-gasless-comment-tokens-20 · video · Gasless Comment Tokens ===== Pitch: Reward insightful video comments with tokens instantly without charging gas fees. Page: https://arcprompts.lovable.app/ideas/video-gasless-comment-tokens-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Comment Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Reward insightful video comments with tokens instantly without charging gas fees. Discipline: Videography & Film (community feedback). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy sponsored transactions enable smooth token rewards for community engagement. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessCommentTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessCommentTokens /// @notice Reward insightful video comments with tokens instantly without charging gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessCommentTokens { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessCommentTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessCommentTokens.sol GaslessCommentTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every community feedback action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-gasless-frame-annotations-23 · video · Gasless Frame Annotations ===== Pitch: Annotate and share feedback on frames instantly with gasless transaction support. Page: https://arcprompts.lovable.app/ideas/video-gasless-frame-annotations-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Frame Annotations" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Annotate and share feedback on frames instantly with gasless transaction support. Discipline: Videography & Film (video feedback). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy sponsored tx remove gas cost barriers for annotation updates. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessFrameAnnotations.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessFrameAnnotations /// @notice Annotate and share feedback on frames instantly with gasless transaction support. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessFrameAnnotations { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessFrameAnnotations` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessFrameAnnotations.sol GaslessFrameAnnotations` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every video feedback action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-gasless-frame-tokens-4 · video · Gasless Frame Tokens ===== Pitch: Mint and trade unique video frames as NFTs without ever paying gas fees. Page: https://arcprompts.lovable.app/ideas/video-gasless-frame-tokens-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Frame Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint and trade unique video frames as NFTs without ever paying gas fees. Discipline: Videography & Film (NFT video frames). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored transactions enable zero-cost NFT minting and transfers for all users. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessFrameTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessFrameTokens /// @notice Mint and trade unique video frames as NFTs without ever paying gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessFrameTokens { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessFrameTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessFrameTokens.sol GaslessFrameTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every NFT video frames action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-gasless-licensing-hub-8 · video · Gasless Licensing Hub ===== Pitch: License stock footage instantly with zero gas fees for hassle-free transactions. Page: https://arcprompts.lovable.app/ideas/video-gasless-licensing-hub-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Licensing Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT License stock footage instantly with zero gas fees for hassle-free transactions. Discipline: Videography & Film (rights management). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored transactions simplify licensing exchanges without user gas cost concerns. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessLicensingHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessLicensingHub /// @notice License stock footage instantly with zero gas fees for hassle-free transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessLicensingHub { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessLicensingHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessLicensingHub.sol GaslessLicensingHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every rights management action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-gasless-release-commits-12 · video · Gasless Release Commits ===== Pitch: Commit video edits onchain transparently with zero gas cost for creators. Page: https://arcprompts.lovable.app/ideas/video-gasless-release-commits-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gasless Release Commits" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Commit video edits onchain transparently with zero gas cost for creators. Discipline: Videography & Film (version control). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy sponsored transactions enable seamless, gasless version tracking. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GaslessReleaseCommits.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGaslessReleaseCommits /// @notice Commit video edits onchain transparently with zero gas cost for creators. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGaslessReleaseCommits { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GaslessReleaseCommits` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GaslessReleaseCommits.sol GaslessReleaseCommits` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every version control action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-gift-provenance-19 · video · GIFt Provenance ===== Pitch: Create creator-owned NFT tokens for animated GIFs to protect and monetize viral clips. Page: https://arcprompts.lovable.app/ideas/video-gift-provenance-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "GIFt Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create creator-owned NFT tokens for animated GIFs to protect and monetize viral clips. Discipline: Videography & Film (animated GIFs). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT minting timestamps GIF IPFS assets, ensuring unique ownership onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GIFtProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title GIFtProvenance /// @notice ERC-721 provenance for: Create creator-owned NFT tokens for animated GIFs to protect and monetize viral clips. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract GIFtProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("GIFtProvenance", "GIFTPR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GIFtProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GIFtProvenance.sol GIFtProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a animated GIFs, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-instant-sponsor-rewards-5 · video · Instant Sponsor Rewards ===== Pitch: Reward fans instantly onchain with tokens during video interactions without gas hurdles. Page: https://arcprompts.lovable.app/ideas/video-instant-sponsor-rewards-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Instant Sponsor Rewards" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Reward fans instantly onchain with tokens during video interactions without gas hurdles. Discipline: Videography & Film (creator monetization). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored tx and Google sign-in facilitate smooth reward issuance and wallet onboarding. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InstantSponsorRewards.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogInstantSponsorRewards /// @notice Reward fans instantly onchain with tokens during video interactions without gas hurdles. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogInstantSponsorRewards { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InstantSponsorRewards` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InstantSponsorRewards.sol InstantSponsorRewards` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every creator monetization action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-lensflare-ledger-11 · video · LensFlare Ledger ===== Pitch: Prove originality of lens flare effects by minting creator-owned tokens of effect presets. Page: https://arcprompts.lovable.app/ideas/video-lensflare-ledger-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LensFlare Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Prove originality of lens flare effects by minting creator-owned tokens of effect presets. Discipline: Videography & Film (visual effects). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain NFT minting timestamps and attributes unique VFX IPFS content securely. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LensFlareLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title LensFlareLedger /// @notice ERC-721 provenance for: Prove originality of lens flare effects by minting creator-owned tokens of effect presets. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract LensFlareLedger is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("LensFlareLedger", "LENSFL") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LensFlareLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LensFlareLedger.sol LensFlareLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a visual effects, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-lensprofile-archive-12 · video · LensProfile Archive ===== Pitch: Decentralize camera and lens profiles on IPFS for consistent color and exposure correction. Page: https://arcprompts.lovable.app/ideas/video-lensprofile-archive-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LensProfile Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Decentralize camera and lens profiles on IPFS for consistent color and exposure correction. Discipline: Videography & Film (camera profiling). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata stores immutable camera profile JSON data accessible by all editing software. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LensProfileArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentLensProfileArchive /// @notice Decentralize camera and lens profiles on IPFS for consistent color and exposure correction. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentLensProfileArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LensProfileArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LensProfileArchive.sol LensProfileArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a camera profiling, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-livecut-chain-18 · video · LiveCut Chain ===== Pitch: Track edits and transitions in live video streams securely onchain for transparent records. Page: https://arcprompts.lovable.app/ideas/video-livecut-chain-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LiveCut Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track edits and transitions in live video streams securely onchain for transparent records. Discipline: Videography & Film (live edit tracking). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts record every live edit event immutably on Sepolia blockchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LiveCutChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title LiveCutChain /// @notice Track edits and transitions in live video streams securely onchain for transparent records. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract LiveCutChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LiveCutChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LiveCutChain.sol LiveCutChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a live edit tracking action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-loopprovenance-10 · video · LoopProvenance ===== Pitch: Store looped animation metadata on IPFS to verify originality and share reusable loop assets. Page: https://arcprompts.lovable.app/ideas/video-loopprovenance-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "LoopProvenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store looped animation metadata on IPFS to verify originality and share reusable loop assets. Discipline: Videography & Film (animation loops). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s JWT upload pins loop descriptors immutably, enabling persistent provenance proof. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LoopProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentLoopProvenance /// @notice Store looped animation metadata on IPFS to verify originality and share reusable loop assets. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentLoopProvenance { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LoopProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LoopProvenance.sol LoopProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a animation loops, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-metadatamesh-ipfs-22 · video · MetadataMesh IPFS ===== Pitch: Pin enriched metadata sets on IPFS to enhance archival, search, and retrieval of video content. Page: https://arcprompts.lovable.app/ideas/video-metadatamesh-ipfs-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MetadataMesh IPFS" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin enriched metadata sets on IPFS to enhance archival, search, and retrieval of video content. Discipline: Videography & Film (metadata management). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata ensures permanent, decentralized storage of metadata JSON manifests for reliable access. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MetadataMeshIPFS.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMetadataMeshIPFS /// @notice Pin enriched metadata sets on IPFS to enhance archival, search, and retrieval of video content. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMetadataMeshIPFS { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MetadataMeshIPFS` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MetadataMeshIPFS.sol MetadataMeshIPFS` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a metadata management, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-moodboard-mint-8 · video · Moodboard Mint ===== Pitch: Create NFT-backed moodboards to validate and share artistic vision securely with collaborators. Page: https://arcprompts.lovable.app/ideas/video-moodboard-mint-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Moodboard Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create NFT-backed moodboards to validate and share artistic vision securely with collaborators. Discipline: Videography & Film (visual concepting). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance anchors moodboard IPFS CIDs to creators, enabling trusted sharing. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MoodboardMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MoodboardMint /// @notice ERC-721 provenance for: Create NFT-backed moodboards to validate and share artistic vision securely with collaborators. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MoodboardMint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MoodboardMint", "MOODBO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MoodboardMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MoodboardMint.sol MoodboardMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a visual concepting, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-motionmap-token-15 · video · MotionMap Token ===== Pitch: Mint motion tracking data as NFTs to verify originality and usage rights. Page: https://arcprompts.lovable.app/ideas/video-motionmap-token-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MotionMap Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint motion tracking data as NFTs to verify originality and usage rights. Discipline: Videography & Film (motion tracking). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance timestamps IPFS motion data securely onchain, ensuring ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MotionMapToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MotionMapToken /// @notice ERC-721 provenance for: Mint motion tracking data as NFTs to verify originality and usage rights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MotionMapToken is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MotionMapToken", "MOTION") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MotionMapToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MotionMapToken.sol MotionMapToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a motion tracking, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-motionmask-archive-4 · video · MotionMask Archive ===== Pitch: Store and share masking templates on IPFS to reuse and collaborate on complex video effects. Page: https://arcprompts.lovable.app/ideas/video-motionmask-archive-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MotionMask Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store and share masking templates on IPFS to reuse and collaborate on complex video effects. Discipline: Videography & Film (masking and effects). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s JWT upload ensures permanent, decentralized storage of masking data and JSON manifests. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MotionMaskArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMotionMaskArchive /// @notice Store and share masking templates on IPFS to reuse and collaborate on complex video effects. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMotionMaskArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MotionMaskArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MotionMaskArchive.sol MotionMaskArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a masking and effects, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-motiontrack-chain-23 · video · MotionTrack Chain ===== Pitch: Record verified motion tracking data onchain for transparent post-production workflows. Page: https://arcprompts.lovable.app/ideas/video-motiontrack-chain-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MotionTrack Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record verified motion tracking data onchain for transparent post-production workflows. Discipline: Videography & Film (motion metadata). Onchain primitive: Arc smart contract. Why this primitive: Sepolia immutable contracts store precise motion metadata securely and verifiably. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MotionTrackChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title MotionTrackChain /// @notice Record verified motion tracking data onchain for transparent post-production workflows. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MotionTrackChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MotionTrackChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MotionTrackChain.sol MotionTrackChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a motion metadata action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-multicam-mint-22 · video · Multicam Mint ===== Pitch: Mint synchronized multicam edits as NFTs to prove complex edit ownership and authenticity. Page: https://arcprompts.lovable.app/ideas/video-multicam-mint-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Multicam Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint synchronized multicam edits as NFTs to prove complex edit ownership and authenticity. Discipline: Videography & Film (multi-camera editing). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain ERC-721 tokens link multicam IPFS metadata to editors transparently. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MulticamMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MulticamMint /// @notice ERC-721 provenance for: Mint synchronized multicam edits as NFTs to prove complex edit ownership and authenticity. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MulticamMint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MulticamMint", "MULTIC") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MulticamMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MulticamMint.sol MulticamMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a multi-camera editing, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-nftscene-frames-13 · video · NFTScene Frames ===== Pitch: Mint key video frames as NFTs to sell exclusive ownership of iconic moments. Page: https://arcprompts.lovable.app/ideas/video-nftscene-frames-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NFTScene Frames" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint key video frames as NFTs to sell exclusive ownership of iconic moments. Discipline: Videography & Film (unique video moments). Onchain primitive: Arc smart contract. Why this primitive: NFTs on Sepolia uniquely store frame data and provenance securely and transparently. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NFTSceneFrames.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title NFTSceneFrames /// @notice Mint key video frames as NFTs to sell exclusive ownership of iconic moments. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract NFTSceneFrames { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NFTSceneFrames` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NFTSceneFrames.sol NFTSceneFrames` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a unique video moments action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-overlay-origin-16 · video · Overlay Origin ===== Pitch: Protect original graphic overlays by minting each as immutable NFTs with creator attribution. Page: https://arcprompts.lovable.app/ideas/video-overlay-origin-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Overlay Origin" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Protect original graphic overlays by minting each as immutable NFTs with creator attribution. Discipline: Videography & Film (graphic overlays). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens timestamp overlay IPFS files securely under creator control. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/OverlayOrigin.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title OverlayOrigin /// @notice ERC-721 provenance for: Protect original graphic overlays by minting each as immutable NFTs with creator attribution. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract OverlayOrigin is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("OverlayOrigin", "OVERLA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs OverlayOrigin` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/OverlayOrigin.sol OverlayOrigin` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a graphic overlays, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-posterchain-assets-19 · video · PosterChain Assets ===== Pitch: Store and share film poster and thumbnail assets on IPFS for consistent branding across platforms. Page: https://arcprompts.lovable.app/ideas/video-posterchain-assets-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PosterChain Assets" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store and share film poster and thumbnail assets on IPFS for consistent branding across platforms. Discipline: Videography & Film (promotional imagery). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s IPFS pinning secures permanent access to promotional image assets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PosterChainAssets.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPosterChainAssets /// @notice Store and share film poster and thumbnail assets on IPFS for consistent branding across platforms. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPosterChainAssets { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PosterChainAssets` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PosterChainAssets.sol PosterChainAssets` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a promotional imagery, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-privy-clip-bounties-15 · video · Privy Clip Bounties ===== Pitch: Reward contributors for clip submissions instantly with gas-free sponsor transactions. Page: https://arcprompts.lovable.app/ideas/video-privy-clip-bounties-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Privy Clip Bounties" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Reward contributors for clip submissions instantly with gas-free sponsor transactions. Discipline: Videography & Film (crowdsourced footage). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored tx enable instant, frictionless bounty payouts to contributors. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PrivyClipBounties.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPrivyClipBounties /// @notice Reward contributors for clip submissions instantly with gas-free sponsor transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPrivyClipBounties { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PrivyClipBounties` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PrivyClipBounties.sol PrivyClipBounties` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every crowdsourced footage action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-reelrights-dao-21 · video · ReelRights DAO ===== Pitch: Decentralize video copyright decisions and dispute resolutions via governance DAOs. Page: https://arcprompts.lovable.app/ideas/video-reelrights-dao-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ReelRights DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Decentralize video copyright decisions and dispute resolutions via governance DAOs. Discipline: Videography & Film (copyright governance). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts enable secure voting and binding governance outcomes. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ReelRightsDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ReelRightsDAO /// @notice Decentralize video copyright decisions and dispute resolutions via governance DAOs. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ReelRightsDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ReelRightsDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ReelRightsDAO.sol ReelRightsDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a copyright governance action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-royaltysplit-dao-7 · video · RoyaltySplit DAO ===== Pitch: Manage automatic onchain royalty splits among multiple video collaborators transparently. Page: https://arcprompts.lovable.app/ideas/video-royaltysplit-dao-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "RoyaltySplit DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Manage automatic onchain royalty splits among multiple video collaborators transparently. Discipline: Videography & Film (creator royalties). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts enforce programmable payments distributing royalties fairly and instantly. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/RoyaltySplitDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title RoyaltySplitDAO /// @notice Manage automatic onchain royalty splits among multiple video collaborators transparently. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract RoyaltySplitDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs RoyaltySplitDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/RoyaltySplitDAO.sol RoyaltySplitDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a creator royalties action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-scenemint-hub-24 · video · SceneMint Hub ===== Pitch: Mint and trade unique video scenes as NFTs to unlock new revenue streams for creators. Page: https://arcprompts.lovable.app/ideas/video-scenemint-hub-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SceneMint Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint and trade unique video scenes as NFTs to unlock new revenue streams for creators. Discipline: Videography & Film (scene NFT marketplace). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts enable secure minting and transferring of scene-based NFTs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SceneMintHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SceneMintHub /// @notice Mint and trade unique video scenes as NFTs to unlock new revenue streams for creators. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SceneMintHub { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SceneMintHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SceneMintHub.sol SceneMintHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a scene NFT marketplace action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-scenereveal-rights-3 · video · SceneReveal Rights ===== Pitch: Onchain minting proves scene ownership, simplifying licensing and reuse agreements for creators. Page: https://arcprompts.lovable.app/ideas/video-scenereveal-rights-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SceneReveal Rights" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Onchain minting proves scene ownership, simplifying licensing and reuse agreements for creators. Discipline: Videography & Film (scene licensing). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Immutable NFT provenance ensures transparent rights and licensing traces for each scene. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SceneRevealRights.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SceneRevealRights /// @notice ERC-721 provenance for: Onchain minting proves scene ownership, simplifying licensing and reuse agreements for creators. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SceneRevealRights is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SceneRevealRights", "SCENER") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SceneRevealRights` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SceneRevealRights.sol SceneRevealRights` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a scene licensing, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-scenesync-chain-5 · video · SceneSync Chain ===== Pitch: Enable real-time, onchain synchronization of collaborative video editing sessions. Page: https://arcprompts.lovable.app/ideas/video-scenesync-chain-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SceneSync Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable real-time, onchain synchronization of collaborative video editing sessions. Discipline: Videography & Film (collaborative editing). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts coordinate edits and track contributions securely on Sepolia network. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SceneSyncChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SceneSyncChain /// @notice Enable real-time, onchain synchronization of collaborative video editing sessions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SceneSyncChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SceneSyncChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SceneSyncChain.sol SceneSyncChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a collaborative editing action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-scenetag-registry-8 · video · SceneTag Registry ===== Pitch: Register and verify scene tags onchain to improve searchable video content metadata. Page: https://arcprompts.lovable.app/ideas/video-scenetag-registry-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SceneTag Registry" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Register and verify scene tags onchain to improve searchable video content metadata. Discipline: Videography & Film (scene metadata). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts ensure immutable, decentralized tagging of video scenes. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SceneTagRegistry.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SceneTagRegistry /// @notice Register and verify scene tags onchain to improve searchable video content metadata. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SceneTagRegistry { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SceneTagRegistry` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SceneTagRegistry.sol SceneTagRegistry` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a scene metadata action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-sceneunlock-token-15 · video · SceneUnlock Token ===== Pitch: Token-gated access to exclusive video scenes ensuring verified viewer entitlements. Page: https://arcprompts.lovable.app/ideas/video-sceneunlock-token-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SceneUnlock Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Token-gated access to exclusive video scenes ensuring verified viewer entitlements. Discipline: Videography & Film (content access control). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts control permissioned access via token ownership on Sepolia 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SceneUnlockToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SceneUnlockToken /// @notice Token-gated access to exclusive video scenes ensuring verified viewer entitlements. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SceneUnlockToken { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SceneUnlockToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SceneUnlockToken.sol SceneUnlockToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a content access control action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-script-to-screen-sync-7 · video · Script-to-Screen Sync ===== Pitch: Sync script changes and video edits onchain for teams without forcing gas payments. Page: https://arcprompts.lovable.app/ideas/video-script-to-screen-sync-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Script-to-Screen Sync" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Sync script changes and video edits onchain for teams without forcing gas payments. Discipline: Videography & Film (production alignment). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet’s gasless tx make real-time script and edit synchronization user-friendly. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScripttoScreenSync.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogScripttoScreenSync /// @notice Sync script changes and video edits onchain for teams without forcing gas payments. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogScripttoScreenSync { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScripttoScreenSync` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScripttoScreenSync.sol ScripttoScreenSync` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every production alignment action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-scriptchain-ledger-9 · video · ScriptChain Ledger ===== Pitch: Timestamp and verify screenplay versions onchain to protect script authorship claims. Page: https://arcprompts.lovable.app/ideas/video-scriptchain-ledger-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptChain Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Timestamp and verify screenplay versions onchain to protect script authorship claims. Discipline: Videography & Film (screenplay tracking). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts provide immutable proof of screenplay creation and edits. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptChainLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ScriptChainLedger /// @notice Timestamp and verify screenplay versions onchain to protect script authorship claims. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ScriptChainLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptChainLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptChainLedger.sol ScriptChainLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a screenplay tracking action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-scriptproof-token-7 · video · ScriptProof Token ===== Pitch: Secure scripts as creator-owned NFTs to prevent plagiarism and prove original authorship. Page: https://arcprompts.lovable.app/ideas/video-scriptproof-token-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptProof Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Secure scripts as creator-owned NFTs to prevent plagiarism and prove original authorship. Discipline: Videography & Film (script authenticity). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT minting immutably links scripts on IPFS with creators via ERC-721 tokens. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptProofToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ScriptProofToken /// @notice ERC-721 provenance for: Secure scripts as creator-owned NFTs to prevent plagiarism and prove original authorship. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ScriptProofToken is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ScriptProofToken", "SCRIPT") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptProofToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptProofToken.sol ScriptProofToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a script authenticity, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-scriptsync-chain-11 · video · ScriptSync Chain ===== Pitch: Pin and link script revisions and annotations on IPFS for collaborative screenplay editing. Page: https://arcprompts.lovable.app/ideas/video-scriptsync-chain-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptSync Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin and link script revisions and annotations on IPFS for collaborative screenplay editing. Discipline: Videography & Film (script and dialogue). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata ensures immutable, accessible script JSON manifests, improving team collaboration. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptSyncChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentScriptSyncChain /// @notice Pin and link script revisions and annotations on IPFS for collaborative screenplay editing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentScriptSyncChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptSyncChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptSyncChain.sol ScriptSyncChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a script and dialogue, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-seamless-clip-shares-0 · video · Seamless Clip Shares ===== Pitch: Share video edits instantly without gas fees, enabling smooth collaboration among creators. Page: https://arcprompts.lovable.app/ideas/video-seamless-clip-shares-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Seamless Clip Shares" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Share video edits instantly without gas fees, enabling smooth collaboration among creators. Discipline: Videography & Film (collaborative editing). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social + sponsored tx allow frictionless user sign-in and gasless transaction sharing. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SeamlessClipShares.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSeamlessClipShares /// @notice Share video edits instantly without gas fees, enabling smooth collaboration among creators. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSeamlessClipShares { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SeamlessClipShares` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SeamlessClipShares.sol SeamlessClipShares` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every collaborative editing action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-shotlist-ledger-23 · video · ShotList Ledger ===== Pitch: Pin shot lists and scene breakdowns on IPFS for secure, shared production references. Page: https://arcprompts.lovable.app/ideas/video-shotlist-ledger-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ShotList Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin shot lists and scene breakdowns on IPFS for secure, shared production references. Discipline: Videography & Film (production tracking). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata immutable pins guarantee persistent access to shot and scene manifests. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ShotListLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentShotListLedger /// @notice Pin shot lists and scene breakdowns on IPFS for secure, shared production references. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentShotListLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ShotListLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ShotListLedger.sol ShotListLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a production tracking, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-soundsync-chain-5 · video · SoundSync Chain ===== Pitch: Synchronize and store audio-video alignment data on IPFS for seamless collaborative editing. Page: https://arcprompts.lovable.app/ideas/video-soundsync-chain-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SoundSync Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Synchronize and store audio-video alignment data on IPFS for seamless collaborative editing. Discipline: Videography & Film (audio synchronization). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata pins synchronization metadata ensuring permanent, tamper-proof storage accessible to all. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SoundSyncChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentSoundSyncChain /// @notice Synchronize and store audio-video alignment data on IPFS for seamless collaborative editing. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentSoundSyncChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SoundSyncChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SoundSyncChain.sol SoundSyncChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a audio synchronization, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-soundsync-provenance-9 · video · SoundSync Provenance ===== Pitch: Mint sound edits as NFTs linked to videos to verify audio-visual synchronization authenticity. Page: https://arcprompts.lovable.app/ideas/video-soundsync-provenance-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SoundSync Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint sound edits as NFTs linked to videos to verify audio-visual synchronization authenticity. Discipline: Videography & Film (audio synchronization). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 NFTs trace audio IPFS files onchain, certifying sync integrity for creators. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SoundSyncProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SoundSyncProvenance /// @notice ERC-721 provenance for: Mint sound edits as NFTs linked to videos to verify audio-visual synchronization authenticity. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SoundSyncProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SoundSyncProvenance", "SOUNDS") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SoundSyncProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SoundSyncProvenance.sol SoundSyncProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a audio synchronization, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-sponsored-clip-challenges-19 · video · Sponsored Clip Challenges ===== Pitch: Create video challenges rewarding participation instantly with no gas transaction fees. Page: https://arcprompts.lovable.app/ideas/video-sponsored-clip-challenges-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sponsored Clip Challenges" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create video challenges rewarding participation instantly with no gas transaction fees. Discipline: Videography & Film (user engagement). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy gasless tx promote seamless challenge reward distribution and onboarding. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SponsoredClipChallenges.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSponsoredClipChallenges /// @notice Create video challenges rewarding participation instantly with no gas transaction fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSponsoredClipChallenges { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SponsoredClipChallenges` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SponsoredClipChallenges.sol SponsoredClipChallenges` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every user engagement action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-sponsored-collab-invites-14 · video · Sponsored Collab Invites ===== Pitch: Invite collaborators to projects through gas-free, onchain wallet interactions. Page: https://arcprompts.lovable.app/ideas/video-sponsored-collab-invites-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sponsored Collab Invites" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Invite collaborators to projects through gas-free, onchain wallet interactions. Discipline: Videography & Film (team management). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy’s Google sign-in and sponsored tx streamline access without gas complexity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SponsoredCollabInvites.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSponsoredCollabInvites /// @notice Invite collaborators to projects through gas-free, onchain wallet interactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSponsoredCollabInvites { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SponsoredCollabInvites` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SponsoredCollabInvites.sol SponsoredCollabInvites` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every team management action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-sponsored-effects-marketplace-3 · video · Sponsored Effects Marketplace ===== Pitch: Buy and sell video effects licenses seamlessly with gasless wallet transactions. Page: https://arcprompts.lovable.app/ideas/video-sponsored-effects-marketplace-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sponsored Effects Marketplace" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Buy and sell video effects licenses seamlessly with gasless wallet transactions. Discipline: Videography & Film (visual effects trading). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallets with sponsored tx remove gas complexity for effect licensing. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SponsoredEffectsMarketplace.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSponsoredEffectsMarketplace /// @notice Buy and sell video effects licenses seamlessly with gasless wallet transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSponsoredEffectsMarketplace { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SponsoredEffectsMarketplace` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SponsoredEffectsMarketplace.sol SponsoredEffectsMarketplace` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every visual effects trading action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-sponsored-render-credits-22 · video · Sponsored Render Credits ===== Pitch: Trade video render credits instantly via gasless onchain transactions. Page: https://arcprompts.lovable.app/ideas/video-sponsored-render-credits-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sponsored Render Credits" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Trade video render credits instantly via gasless onchain transactions. Discipline: Videography & Film (compute resource sharing). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallets with sponsored tx facilitate smooth render credit exchanges. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SponsoredRenderCredits.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSponsoredRenderCredits /// @notice Trade video render credits instantly via gasless onchain transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSponsoredRenderCredits { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SponsoredRenderCredits` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SponsoredRenderCredits.sol SponsoredRenderCredits` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every compute resource sharing action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-sponsored-review-tokens-10 · video · Sponsored Review Tokens ===== Pitch: Viewers reward video feedback with tokens instantly, without seeing transaction fees. Page: https://arcprompts.lovable.app/ideas/video-sponsored-review-tokens-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sponsored Review Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Viewers reward video feedback with tokens instantly, without seeing transaction fees. Discipline: Videography & Film (audience feedback). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet with gasless tx enables frictionless token reward distribution. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SponsoredReviewTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSponsoredReviewTokens /// @notice Viewers reward video feedback with tokens instantly, without seeing transaction fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSponsoredReviewTokens { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SponsoredReviewTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SponsoredReviewTokens.sol SponsoredReviewTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every audience feedback action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-storyboard-chain-2 · video · Storyboard Chain ===== Pitch: Create IPFS-backed interactive storyboards that persistently link video plans and references. Page: https://arcprompts.lovable.app/ideas/video-storyboard-chain-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Storyboard Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create IPFS-backed interactive storyboards that persistently link video plans and references. Discipline: Videography & Film (preproduction planning). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata stores storyboards as JSON/manifests ensuring persistent, easily shareable preproduction records. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StoryboardChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentStoryboardChain /// @notice Create IPFS-backed interactive storyboards that persistently link video plans and references. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentStoryboardChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StoryboardChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StoryboardChain.sol StoryboardChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a preproduction planning, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-storyboard-stamp-4 · video · Storyboard Stamp ===== Pitch: Mint storyboards as NFT tokens to secure creative vision and enable easy sharing with teams. Page: https://arcprompts.lovable.app/ideas/video-storyboard-stamp-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Storyboard Stamp" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint storyboards as NFT tokens to secure creative vision and enable easy sharing with teams. Discipline: Videography & Film (previsualization). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT minting onchain timestamps storyboards with IPFS CIDs, verifying original creative concepts. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StoryboardStamp.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title StoryboardStamp /// @notice ERC-721 provenance for: Mint storyboards as NFT tokens to secure creative vision and enable easy sharing with teams. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StoryboardStamp is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("StoryboardStamp", "STORYB") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StoryboardStamp` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StoryboardStamp.sol StoryboardStamp` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a previsualization, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-subtitle-signet-17 · video · Subtitle Signet ===== Pitch: Mint subtitles as NFTs to certify translation accuracy and original captioning ownership. Page: https://arcprompts.lovable.app/ideas/video-subtitle-signet-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Subtitle Signet" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint subtitles as NFTs to certify translation accuracy and original captioning ownership. Discipline: Videography & Film (captioning). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain NFT minting links subtitle IPFS files to creators, ensuring verifiable origin. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SubtitleSignet.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SubtitleSignet /// @notice ERC-721 provenance for: Mint subtitles as NFTs to certify translation accuracy and original captioning ownership. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SubtitleSignet is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SubtitleSignet", "SUBTIT") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SubtitleSignet` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SubtitleSignet.sol SubtitleSignet` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a captioning, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-timelapse-token-21 · video · TimeLapse Token ===== Pitch: Secure time-lapse video originality by minting as NFTs with immutable timestamps. Page: https://arcprompts.lovable.app/ideas/video-timelapse-token-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "TimeLapse Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Secure time-lapse video originality by minting as NFTs with immutable timestamps. Discipline: Videography & Film (time-lapse videography). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance mints IPFS time-lapse assets, certifying long-duration capture ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TimeLapseToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title TimeLapseToken /// @notice ERC-721 provenance for: Secure time-lapse video originality by minting as NFTs with immutable timestamps. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TimeLapseToken is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("TimeLapseToken", "TIMELA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TimeLapseToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TimeLapseToken.sol TimeLapseToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a time-lapse videography, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-timelinetrace-ipfs-16 · video · TimelineTrace IPFS ===== Pitch: Pin entire editing timelines on IPFS to enable persistent sharing and rollback across projects. Page: https://arcprompts.lovable.app/ideas/video-timelinetrace-ipfs-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "TimelineTrace IPFS" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin entire editing timelines on IPFS to enable persistent sharing and rollback across projects. Discipline: Videography & Film (editing timeline). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata stores immutable timeline manifests ensuring long-term availability and version tracking. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TimelineTraceIPFS.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentTimelineTraceIPFS /// @notice Pin entire editing timelines on IPFS to enable persistent sharing and rollback across projects. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentTimelineTraceIPFS { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TimelineTraceIPFS` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TimelineTraceIPFS.sol TimelineTraceIPFS` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a editing timeline, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-titletrack-tokens-10 · video · TitleTrack Tokens ===== Pitch: Mint unique title animations NFTs to protect and share original animated intros. Page: https://arcprompts.lovable.app/ideas/video-titletrack-tokens-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "TitleTrack Tokens" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint unique title animations NFTs to protect and share original animated intros. Discipline: Videography & Film (title animation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance records title animation IPFS assets with creator ownership onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TitleTrackTokens.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title TitleTrackTokens /// @notice ERC-721 provenance for: Mint unique title animations NFTs to protect and share original animated intros. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TitleTrackTokens is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("TitleTrackTokens", "TITLET") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TitleTrackTokens` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TitleTrackTokens.sol TitleTrackTokens` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a title animation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-token-gate-premieres-18 · video · Token Gate Premieres ===== Pitch: Host video premieres gated by token ownership with smooth, gasless access. Page: https://arcprompts.lovable.app/ideas/video-token-gate-premieres-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Token Gate Premieres" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Host video premieres gated by token ownership with smooth, gasless access. Discipline: Videography & Film (exclusive screenings). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsor tx enable premium access control without requiring users to pay gas. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TokenGatePremieres.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogTokenGatePremieres /// @notice Host video premieres gated by token ownership with smooth, gasless access. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogTokenGatePremieres { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TokenGatePremieres` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TokenGatePremieres.sol TokenGatePremieres` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every exclusive screenings action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-tokenized-b-roll-access-13 · video · Tokenized B-Roll Access ===== Pitch: Grant and revoke B-roll access quickly via token ownership with no gas fees. Page: https://arcprompts.lovable.app/ideas/video-tokenized-b-roll-access-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Tokenized B-Roll Access" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Grant and revoke B-roll access quickly via token ownership with no gas fees. Discipline: Videography & Film (footage licensing). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy gasless wallet enables instant token-based permission management. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TokenizedBRollAccess.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogTokenizedBRollAccess /// @notice Grant and revoke B-roll access quickly via token ownership with no gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogTokenizedBRollAccess { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TokenizedBRollAccess` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TokenizedBRollAccess.sol TokenizedBRollAccess` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every footage licensing action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-tokenized-cut-approval-1 · video · Tokenized Cut Approval ===== Pitch: Enable editors and clients to approve cuts securely via onchain, without gas barriers. Page: https://arcprompts.lovable.app/ideas/video-tokenized-cut-approval-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Tokenized Cut Approval" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable editors and clients to approve cuts securely via onchain, without gas barriers. Discipline: Videography & Film (edit review). Onchain primitive: Privy social wallet on Arc. Why this primitive: Gasless sponsored tx ensure approval actions are user-friendly and trackable onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TokenizedCutApproval.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogTokenizedCutApproval /// @notice Enable editors and clients to approve cuts securely via onchain, without gas barriers. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogTokenizedCutApproval { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TokenizedCutApproval` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TokenizedCutApproval.sol TokenizedCutApproval` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every edit review action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-tokenized-music-sync-24 · video · Tokenized Music Sync ===== Pitch: License music for videos via tokens with zero gas fees and seamless sign-in. Page: https://arcprompts.lovable.app/ideas/video-tokenized-music-sync-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Tokenized Music Sync" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT License music for videos via tokens with zero gas fees and seamless sign-in. Discipline: Videography & Film (audio licensing). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy gasless tx simplify music sync licensing for creators and publishers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TokenizedMusicSync.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogTokenizedMusicSync /// @notice License music for videos via tokens with zero gas fees and seamless sign-in. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogTokenizedMusicSync { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TokenizedMusicSync` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TokenizedMusicSync.sol TokenizedMusicSync` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every audio licensing action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-trailertrace-mint-18 · video · TrailerTrace Mint ===== Pitch: Securely mint promotional trailers as NFTs to confirm authenticity and release dates. Page: https://arcprompts.lovable.app/ideas/video-trailertrace-mint-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "TrailerTrace Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely mint promotional trailers as NFTs to confirm authenticity and release dates. Discipline: Videography & Film (promotional cuts). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance records trailer IPFS CIDs onchain, linking to official creators. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TrailerTraceMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title TrailerTraceMint /// @notice ERC-721 provenance for: Securely mint promotional trailers as NFTs to confirm authenticity and release dates. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TrailerTraceMint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("TrailerTraceMint", "TRAILE") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TrailerTraceMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TrailerTraceMint.sol TrailerTraceMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a promotional cuts, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-transcode-manifest-9 · video · Transcode Manifest ===== Pitch: Pin transcoding manifests on IPFS to track formats, resolutions, and settings transparently. Page: https://arcprompts.lovable.app/ideas/video-transcode-manifest-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Transcode Manifest" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin transcoding manifests on IPFS to track formats, resolutions, and settings transparently. Discipline: Videography & Film (video encoding). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata guarantees permanent, tamper-proof storage of transcoding metadata for reliable versioning. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TranscodeManifest.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentTranscodeManifest /// @notice Pin transcoding manifests on IPFS to track formats, resolutions, and settings transparently. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentTranscodeManifest { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TranscodeManifest` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TranscodeManifest.sol TranscodeManifest` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a video encoding, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-transitionchain-vault-21 · video · TransitionChain Vault ===== Pitch: Store reusable video transition presets on IPFS to ensure permanent, shareable effects. Page: https://arcprompts.lovable.app/ideas/video-transitionchain-vault-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "TransitionChain Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store reusable video transition presets on IPFS to ensure permanent, shareable effects. Discipline: Videography & Film (transition effects). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata pins transition data immutably, facilitating collaboration and versioning. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TransitionChainVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentTransitionChainVault /// @notice Store reusable video transition presets on IPFS to ensure permanent, shareable effects. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentTransitionChainVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TransitionChainVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TransitionChainVault.sol TransitionChainVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a transition effects, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-tutorialtoken-vault-23 · video · TutorialToken Vault ===== Pitch: Mint tutorial videos as NFTs to certify original teaching content and secure creator rights. Page: https://arcprompts.lovable.app/ideas/video-tutorialtoken-vault-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "TutorialToken Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint tutorial videos as NFTs to certify original teaching content and secure creator rights. Discipline: Videography & Film (educational content). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance anchors IPFS-hosted tutorials onchain to creators securely. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TutorialTokenVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title TutorialTokenVault /// @notice ERC-721 provenance for: Mint tutorial videos as NFTs to certify original teaching content and secure creator rights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract TutorialTokenVault is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("TutorialTokenVault", "TUTORI") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TutorialTokenVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TutorialTokenVault.sol TutorialTokenVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a educational content, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-verified-creator-ids-9 · video · Verified Creator IDs ===== Pitch: Enable filmmakers to verify credentials onchain with easy sign-in and no gas fees. Page: https://arcprompts.lovable.app/ideas/video-verified-creator-ids-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Verified Creator IDs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable filmmakers to verify credentials onchain with easy sign-in and no gas fees. Discipline: Videography & Film (identity verification). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social's Google sign-in plus sponsored tx secures ID verification smoothly. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VerifiedCreatorIDs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogVerifiedCreatorIDs /// @notice Enable filmmakers to verify credentials onchain with easy sign-in and no gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogVerifiedCreatorIDs { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VerifiedCreatorIDs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VerifiedCreatorIDs.sol VerifiedCreatorIDs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every identity verification action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-vfx-asset-chain-7 · video · VFX Asset Chain ===== Pitch: Decentralize and share VFX assets metadata on IPFS to streamline collaborative asset management. Page: https://arcprompts.lovable.app/ideas/video-vfx-asset-chain-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VFX Asset Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Decentralize and share VFX assets metadata on IPFS to streamline collaborative asset management. Discipline: Videography & Film (visual effects). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata stores immutable asset manifests on IPFS, ensuring persistent availability across teams. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VFXAssetChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentVFXAssetChain /// @notice Decentralize and share VFX assets metadata on IPFS to streamline collaborative asset management. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentVFXAssetChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VFXAssetChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VFXAssetChain.sol VFXAssetChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a visual effects, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-videocollab-dao-12 · video · VideoCollab DAO ===== Pitch: Decentralize video project management via DAOs for transparent decision-making and funding. Page: https://arcprompts.lovable.app/ideas/video-videocollab-dao-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VideoCollab DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Decentralize video project management via DAOs for transparent decision-making and funding. Discipline: Videography & Film (collaborative project management). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts enable programmable governance and funding distribution. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VideoCollabDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title VideoCollabDAO /// @notice Decentralize video project management via DAOs for transparent decision-making and funding. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VideoCollabDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VideoCollabDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VideoCollabDAO.sol VideoCollabDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a collaborative project management action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA video-vlogproof-mint-14 · video · VlogProof Mint ===== Pitch: Authenticate vlogs via NFT tokens to assure viewers of original creator ownership and date. Page: https://arcprompts.lovable.app/ideas/video-vlogproof-mint-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VlogProof Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate vlogs via NFT tokens to assure viewers of original creator ownership and date. Discipline: Videography & Film (content authenticity). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain NFT minting links vlogging IPFS content to creator wallets for proof. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VlogProofMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title VlogProofMint /// @notice ERC-721 provenance for: Authenticate vlogs via NFT tokens to assure viewers of original creator ownership and date. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VlogProofMint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("VlogProofMint", "VLOGPR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VlogProofMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VlogProofMint.sol VlogProofMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a content authenticity, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-art-commission-chain-10 · visual-art · Art Commission Chain ===== Pitch: Streamline and secure commission contracts and payments between artists and patrons using smart contracts. Page: https://arcprompts.lovable.app/ideas/visual-art-art-commission-chain-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Art Commission Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Streamline and secure commission contracts and payments between artists and patrons using smart contracts. Discipline: Visual Art (commission workflow management). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts automate escrow and milestone payments trustlessly. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ArtCommissionChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ArtCommissionChain /// @notice Streamline and secure commission contracts and payments between artists and patrons using smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ArtCommissionChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ArtCommissionChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ArtCommissionChain.sol ArtCommissionChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a commission workflow management action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-art-critique-chain-13 · visual-art · Art Critique Chain ===== Pitch: Record artist peer critiques and feedback permanently and transparently onchain. Page: https://arcprompts.lovable.app/ideas/visual-art-art-critique-chain-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Art Critique Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record artist peer critiques and feedback permanently and transparently onchain. Discipline: Visual Art (peer review logging). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts ensure immutable and censorship-resistant critique logs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ArtCritiqueChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ArtCritiqueChain /// @notice Record artist peer critiques and feedback permanently and transparently onchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ArtCritiqueChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ArtCritiqueChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ArtCritiqueChain.sol ArtCritiqueChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a peer review logging action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-art-fair-digital-catalog-13 · visual-art · Art Fair Digital Catalog ===== Pitch: Create and pin permanent digital catalogs for art fairs accessible globally. Page: https://arcprompts.lovable.app/ideas/visual-art-art-fair-digital-catalog-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Art Fair Digital Catalog" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create and pin permanent digital catalogs for art fairs accessible globally. Discipline: Visual Art (event art listings). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT pinning guarantees immutable, decentralized access to fair catalogs and data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ArtFairDigitalCatalog.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentArtFairDigitalCatalog /// @notice Create and pin permanent digital catalogs for art fairs accessible globally. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentArtFairDigitalCatalog { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ArtFairDigitalCatalog` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ArtFairDigitalCatalog.sol ArtFairDigitalCatalog` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a event art listings, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-art-mentor-mark-17 · visual-art · Art Mentor Mark ===== Pitch: Mint NFTs to certify mentorship lineage and creative influence among artists. Page: https://arcprompts.lovable.app/ideas/visual-art-art-mentor-mark-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Art Mentor Mark" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint NFTs to certify mentorship lineage and creative influence among artists. Discipline: Visual Art (art mentorship proof). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens create verifiable mentorship provenance anchored on the blockchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ArtMentorMark.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ArtMentorMark /// @notice ERC-721 provenance for: Mint NFTs to certify mentorship lineage and creative influence among artists. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ArtMentorMark is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ArtMentorMark", "ARTMEN") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ArtMentorMark` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ArtMentorMark.sol ArtMentorMark` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a art mentorship proof, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-art-rewind-20 · visual-art · Art Rewind ===== Pitch: Mint stepwise NFTs documenting artwork creation stages for replay and provenance. Page: https://arcprompts.lovable.app/ideas/visual-art-art-rewind-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Art Rewind" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint stepwise NFTs documenting artwork creation stages for replay and provenance. Discipline: Visual Art (creative process playback). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 minting onchain links IPFS CIDs capturing each creative moment forever. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ArtRewind.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ArtRewind /// @notice ERC-721 provenance for: Mint stepwise NFTs documenting artwork creation stages for replay and provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ArtRewind is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ArtRewind", "ARTREW") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ArtRewind` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ArtRewind.sol ArtRewind` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a creative process playback, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-art-swap-escrow-23 · visual-art · Art Swap Escrow ===== Pitch: Allow artists and collectors to trade artworks using blockchain escrow ensuring transaction safety. Page: https://arcprompts.lovable.app/ideas/visual-art-art-swap-escrow-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Art Swap Escrow" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Allow artists and collectors to trade artworks using blockchain escrow ensuring transaction safety. Discipline: Visual Art (secure art trades). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide trustless escrow with automatic release upon conditions met. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ArtSwapEscrow.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ArtSwapEscrow /// @notice Allow artists and collectors to trade artworks using blockchain escrow ensuring transaction safety. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ArtSwapEscrow { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ArtSwapEscrow` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ArtSwapEscrow.sol ArtSwapEscrow` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a secure art trades action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-art-swap-protocol-8 · visual-art · Art Swap Protocol ===== Pitch: Create a trustless platform for artists and collectors to swap artworks instantly onchain. Page: https://arcprompts.lovable.app/ideas/visual-art-art-swap-protocol-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Art Swap Protocol" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create a trustless platform for artists and collectors to swap artworks instantly onchain. Discipline: Visual Art (peer-to-peer art exchange). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enable atomic swaps guaranteeing secure, simultaneous asset exchanges. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ArtSwapProtocol.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ArtSwapProtocol /// @notice Create a trustless platform for artists and collectors to swap artworks instantly onchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ArtSwapProtocol { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ArtSwapProtocol` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ArtSwapProtocol.sol ArtSwapProtocol` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a peer-to-peer art exchange action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-artchain-auction-15 · visual-art · Artchain Auction ===== Pitch: Integrate provenance minting into art auctions to verify authenticity and creator royalties. Page: https://arcprompts.lovable.app/ideas/visual-art-artchain-auction-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Artchain Auction" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Integrate provenance minting into art auctions to verify authenticity and creator royalties. Discipline: Visual Art (secondary art sales). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain NFT provenance mints enable transparent ownership and royalty enforcement. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ArtchainAuction.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ArtchainAuction /// @notice ERC-721 provenance for: Integrate provenance minting into art auctions to verify authenticity and creator royalties. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ArtchainAuction is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ArtchainAuction", "ARTCHA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ArtchainAuction` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ArtchainAuction.sol ArtchainAuction` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a secondary art sales, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-artchain-auctions-10 · visual-art · ArtChain Auctions ===== Pitch: Create gasless onchain auctions for visual artists with fast wallet onboarding and sponsored bids. Page: https://arcprompts.lovable.app/ideas/visual-art-artchain-auctions-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ArtChain Auctions" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create gasless onchain auctions for visual artists with fast wallet onboarding and sponsored bids. Discipline: Visual Art (auction facilitation). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored transactions prevent bidding friction from gas fees. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ArtChainAuctions.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogArtChainAuctions /// @notice Create gasless onchain auctions for visual artists with fast wallet onboarding and sponsored bids. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogArtChainAuctions { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ArtChainAuctions` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ArtChainAuctions.sol ArtChainAuctions` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every auction facilitation action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-artchain-critique-18 · visual-art · ArtChain Critique ===== Pitch: Painters share work for peer feedback with onchain reputation and gasless login/transactions. Page: https://arcprompts.lovable.app/ideas/visual-art-artchain-critique-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ArtChain Critique" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Painters share work for peer feedback with onchain reputation and gasless login/transactions. Discipline: Visual Art (peer review platform). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet plus sponsored tx enable frictionless participation and reputation building. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ArtChainCritique.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogArtChainCritique /// @notice Painters share work for peer feedback with onchain reputation and gasless login/transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogArtChainCritique { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ArtChainCritique` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ArtChainCritique.sol ArtChainCritique` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every peer review platform action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-artistic-collaboration-chain-22 · visual-art · Artistic Collaboration Chain ===== Pitch: Pin and permanently store co-created artworks and contribution metadata for fair crediting. Page: https://arcprompts.lovable.app/ideas/visual-art-artistic-collaboration-chain-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Artistic Collaboration Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin and permanently store co-created artworks and contribution metadata for fair crediting. Discipline: Visual Art (joint creation records). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT multi-upload supports joint data pinning with immutable records. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ArtisticCollaborationChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentArtisticCollaborationChain /// @notice Pin and permanently store co-created artworks and contribution metadata for fair crediting. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentArtisticCollaborationChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ArtisticCollaborationChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ArtisticCollaborationChain.sol ArtisticCollaborationChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a joint creation records, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-artswap-network-23 · visual-art · ArtSwap Network ===== Pitch: Enable painters and illustrators to swap artworks onchain with gasless wallet bootstrapped by social login. Page: https://arcprompts.lovable.app/ideas/visual-art-artswap-network-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ArtSwap Network" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable painters and illustrators to swap artworks onchain with gasless wallet bootstrapped by social login. Discipline: Visual Art (visual art trades). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored tx guarantee frictionless, cost-free art trades. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ArtSwapNetwork.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogArtSwapNetwork /// @notice Enable painters and illustrators to swap artworks onchain with gasless wallet bootstrapped by social login. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogArtSwapNetwork { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ArtSwapNetwork` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ArtSwapNetwork.sol ArtSwapNetwork` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every visual art trades action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-artswap-social-7 · visual-art · ArtSwap Social ===== Pitch: Build a social marketplace for illustrators to swap artworks and services without gas concerns. Page: https://arcprompts.lovable.app/ideas/visual-art-artswap-social-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ArtSwap Social" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Build a social marketplace for illustrators to swap artworks and services without gas concerns. Discipline: Visual Art (art barter communities). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet and sponsored tx facilitate frictionless, gasless peer-to-peer swaps. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ArtSwapSocial.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogArtSwapSocial /// @notice Build a social marketplace for illustrators to swap artworks and services without gas concerns. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogArtSwapSocial { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ArtSwapSocial` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ArtSwapSocial.sol ArtSwapSocial` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every art barter communities action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-auction-trust-chain-4 · visual-art · Auction Trust Chain ===== Pitch: Enable transparent, verifiable auction processes and bids for visual art onchain. Page: https://arcprompts.lovable.app/ideas/visual-art-auction-trust-chain-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Auction Trust Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable transparent, verifiable auction processes and bids for visual art onchain. Discipline: Visual Art (art auction transparency). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enforce fair bid logic and record bids immutably. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AuctionTrustChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AuctionTrustChain /// @notice Enable transparent, verifiable auction processes and bids for visual art onchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AuctionTrustChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AuctionTrustChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AuctionTrustChain.sol AuctionTrustChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a art auction transparency action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-augmented-art-archive-16 · visual-art · Augmented Art Archive ===== Pitch: Pin AR textures, markers, and metadata permanently to IPFS for reliable augmented experiences. Page: https://arcprompts.lovable.app/ideas/visual-art-augmented-art-archive-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Augmented Art Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin AR textures, markers, and metadata permanently to IPFS for reliable augmented experiences. Discipline: Visual Art (AR art content storage). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata supports permanent pinning of large AR asset files with consistent CID referencing. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AugmentedArtArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAugmentedArtArchive /// @notice Pin AR textures, markers, and metadata permanently to IPFS for reliable augmented experiences. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAugmentedArtArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AugmentedArtArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AugmentedArtArchive.sol AugmentedArtArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a AR art content storage, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-auto-manifest-builder-10 · visual-art · Auto-Manifest Builder ===== Pitch: Automatically generate and pin complete IPFS manifests for artists’ works with minimal effort. Page: https://arcprompts.lovable.app/ideas/visual-art-auto-manifest-builder-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Auto-Manifest Builder" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Automatically generate and pin complete IPFS manifests for artists’ works with minimal effort. Discipline: Visual Art (metadata automation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT API allows easy automated pins for bulk metadata and image uploads. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AutoManifestBuilder.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentAutoManifestBuilder /// @notice Automatically generate and pin complete IPFS manifests for artists’ works with minimal effort. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentAutoManifestBuilder { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AutoManifestBuilder` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AutoManifestBuilder.sol AutoManifestBuilder` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a metadata automation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-brushbot-16 · visual-art · BrushBot ===== Pitch: Integrate AI painting tools powered by gasless onchain user verification and transactions. Page: https://arcprompts.lovable.app/ideas/visual-art-brushbot-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "BrushBot" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Integrate AI painting tools powered by gasless onchain user verification and transactions. Discipline: Visual Art (AI-assisted painting). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet and sponsored tx provide smooth tool access without blockchain complexity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/BrushBot.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogBrushBot /// @notice Integrate AI painting tools powered by gasless onchain user verification and transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogBrushBot { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs BrushBot` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/BrushBot.sol BrushBot` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every AI-assisted painting action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-brushstroke-chain-5 · visual-art · Brushstroke Chain ===== Pitch: Record and mint signature brushstroke styles as NFTs for artist branding and licensing. Page: https://arcprompts.lovable.app/ideas/visual-art-brushstroke-chain-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Brushstroke Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record and mint signature brushstroke styles as NFTs for artist branding and licensing. Discipline: Visual Art (brush technique archives). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens verify authentic brushstroke styles with permanent on-chain IPFS proof. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/BrushstrokeChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title BrushstrokeChain /// @notice ERC-721 provenance for: Record and mint signature brushstroke styles as NFTs for artist branding and licensing. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract BrushstrokeChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("BrushstrokeChain", "BRUSHS") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs BrushstrokeChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/BrushstrokeChain.sol BrushstrokeChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a brush technique archives, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-brushstroke-dao-vault-22 · visual-art · Brushstroke DAO Vault ===== Pitch: Pool resources in decentralized vaults to fund promising visual art projects transparently. Page: https://arcprompts.lovable.app/ideas/visual-art-brushstroke-dao-vault-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Brushstroke DAO Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pool resources in decentralized vaults to fund promising visual art projects transparently. Discipline: Visual Art (collective art funding). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts allow secure fund management and voting by contributors. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/BrushstrokeDAOVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title BrushstrokeDAOVault /// @notice Pool resources in decentralized vaults to fund promising visual art projects transparently. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract BrushstrokeDAOVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs BrushstrokeDAOVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/BrushstrokeDAOVault.sol BrushstrokeDAOVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a collective art funding action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-brushstroke-rights-9 · visual-art · Brushstroke Rights ===== Pitch: Help illustrators manage and transfer licenses onchain with simple Google sign-in and zero gas. Page: https://arcprompts.lovable.app/ideas/visual-art-brushstroke-rights-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Brushstroke Rights" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Help illustrators manage and transfer licenses onchain with simple Google sign-in and zero gas. Discipline: Visual Art (digital rights management). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy primitive ensures easy rights transfers without user knowledge of gas or keys. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/BrushstrokeRights.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogBrushstrokeRights /// @notice Help illustrators manage and transfer licenses onchain with simple Google sign-in and zero gas. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogBrushstrokeRights { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs BrushstrokeRights` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/BrushstrokeRights.sol BrushstrokeRights` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every digital rights management action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-brushstroke-timestamp-5 · visual-art · Brushstroke Timestamp ===== Pitch: Document and timestamp artists’ brushstroke progress live onchain for proof of creation. Page: https://arcprompts.lovable.app/ideas/visual-art-brushstroke-timestamp-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Brushstroke Timestamp" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Document and timestamp artists’ brushstroke progress live onchain for proof of creation. Discipline: Visual Art (creative process timestamping). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide reliable, immutable time-stamped events during creation. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/BrushstrokeTimestamp.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title BrushstrokeTimestamp /// @notice Document and timestamp artists’ brushstroke progress live onchain for proof of creation. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract BrushstrokeTimestamp { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs BrushstrokeTimestamp` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/BrushstrokeTimestamp.sol BrushstrokeTimestamp` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a creative process timestamping action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-canvas-chronicles-0 · visual-art · Canvas Chronicles ===== Pitch: Securely mint and track authentic painting portfolios with verifiable creation history. Page: https://arcprompts.lovable.app/ideas/visual-art-canvas-chronicles-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Canvas Chronicles" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely mint and track authentic painting portfolios with verifiable creation history. Discipline: Visual Art (painting portfolios). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance mint ensures immutable creator ownership and art authenticity 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CanvasChronicles.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title CanvasChronicles /// @notice ERC-721 provenance for: Securely mint and track authentic painting portfolios with verifiable creation history. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CanvasChronicles is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("CanvasChronicles", "CANVAS") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CanvasChronicles` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CanvasChronicles.sol CanvasChronicles` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a painting portfolios, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-canvas-collaborate-0 · visual-art · Canvas Collaborate ===== Pitch: Enable painters to co-create artworks with seamless onchain identity and gasless contributions. Page: https://arcprompts.lovable.app/ideas/visual-art-canvas-collaborate-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Canvas Collaborate" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable painters to co-create artworks with seamless onchain identity and gasless contributions. Discipline: Visual Art (collaborative painting). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored transactions allow smooth collaboration without gas friction or complex wallets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CanvasCollaborate.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogCanvasCollaborate /// @notice Enable painters to co-create artworks with seamless onchain identity and gasless contributions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogCanvasCollaborate { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CanvasCollaborate` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CanvasCollaborate.sol CanvasCollaborate` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every collaborative painting action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-canvas-rental-token-20 · visual-art · Canvas Rental Token ===== Pitch: Tokenize and manage temporary rental of studio or gallery spaces for artists seamlessly onchain. Page: https://arcprompts.lovable.app/ideas/visual-art-canvas-rental-token-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Canvas Rental Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Tokenize and manage temporary rental of studio or gallery spaces for artists seamlessly onchain. Discipline: Visual Art (art space access). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enable secure timed access with programmable expiration and transfers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CanvasRentalToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title CanvasRentalToken /// @notice Tokenize and manage temporary rental of studio or gallery spaces for artists seamlessly onchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CanvasRentalToken { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CanvasRentalToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CanvasRentalToken.sol CanvasRentalToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a art space access action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-collaborative-art-archive-11 · visual-art · Collaborative Art Archive ===== Pitch: Pin shared artworks and process files to IPFS for permanent collaborative history storage. Page: https://arcprompts.lovable.app/ideas/visual-art-collaborative-art-archive-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Collaborative Art Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin shared artworks and process files to IPFS for permanent collaborative history storage. Discipline: Visual Art (group project documentation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT supports multi-user authenticated uploads securing collaboration records. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CollaborativeArtArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentCollaborativeArtArchive /// @notice Pin shared artworks and process files to IPFS for permanent collaborative history storage. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentCollaborativeArtArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CollaborativeArtArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CollaborativeArtArchive.sol CollaborativeArtArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a group project documentation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-color-code-vault-16 · visual-art · Color Code Vault ===== Pitch: Securely store and prove origin of proprietary digital color codes with mintable NFTs. Page: https://arcprompts.lovable.app/ideas/visual-art-color-code-vault-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Color Code Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely store and prove origin of proprietary digital color codes with mintable NFTs. Discipline: Visual Art (digital color codes). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 provenance mint guarantees creator ownership and IPFS hash immutability. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ColorCodeVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ColorCodeVault /// @notice ERC-721 provenance for: Securely store and prove origin of proprietary digital color codes with mintable NFTs. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ColorCodeVault is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ColorCodeVault", "COLORC") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ColorCodeVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ColorCodeVault.sol ColorCodeVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a digital color codes, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-color-ledger-4 · visual-art · Color Ledger ===== Pitch: Mint unique NFTs capturing and securing artist-specific color palettes and schemes. Page: https://arcprompts.lovable.app/ideas/visual-art-color-ledger-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Color Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint unique NFTs capturing and securing artist-specific color palettes and schemes. Discipline: Visual Art (color palette preservation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance mint anchors palette data immutably linked to the creator’s token. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ColorLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ColorLedger /// @notice ERC-721 provenance for: Mint unique NFTs capturing and securing artist-specific color palettes and schemes. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ColorLedger is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ColorLedger", "COLORL") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ColorLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ColorLedger.sol ColorLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a color palette preservation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-colorchain-auctions-20 · visual-art · Colorchain Auctions ===== Pitch: Host auctions of color-driven artworks with gasless bidding and quick wallet onboarding. Page: https://arcprompts.lovable.app/ideas/visual-art-colorchain-auctions-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Colorchain Auctions" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Host auctions of color-driven artworks with gasless bidding and quick wallet onboarding. Discipline: Visual Art (color-themed art auctions). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet and sponsored transactions remove gas fee barriers in niche auctions. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ColorchainAuctions.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogColorchainAuctions /// @notice Host auctions of color-driven artworks with gasless bidding and quick wallet onboarding. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogColorchainAuctions { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ColorchainAuctions` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ColorchainAuctions.sol ColorchainAuctions` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every color-themed art auctions action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-colorcode-certifier-3 · visual-art · ColorCode Certifier ===== Pitch: Certify and verify exact color compositions of artworks using blockchain color code hashes. Page: https://arcprompts.lovable.app/ideas/visual-art-colorcode-certifier-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ColorCode Certifier" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Certify and verify exact color compositions of artworks using blockchain color code hashes. Discipline: Visual Art (color authenticity validation). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts guarantee immutable storage of color code data linked to artist hashes. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ColorCodeCertifier.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ColorCodeCertifier /// @notice Certify and verify exact color compositions of artworks using blockchain color code hashes. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ColorCodeCertifier { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ColorCodeCertifier` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ColorCodeCertifier.sol ColorCodeCertifier` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a color authenticity validation action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-colorstory-13 · visual-art · ColorStory ===== Pitch: Illustrators share evolving color-based stories secured onchain with gasless updates and Google sign-in. Page: https://arcprompts.lovable.app/ideas/visual-art-colorstory-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ColorStory" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Illustrators share evolving color-based stories secured onchain with gasless updates and Google sign-in. Discipline: Visual Art (color narrative art). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet and sponsored tx allow smooth story versioning without transaction costs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ColorStory.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogColorStory /// @notice Illustrators share evolving color-based stories secured onchain with gasless updates and Google sign-in. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogColorStory { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ColorStory` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ColorStory.sol ColorStory` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every color narrative art action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-curator-s-immutable-log-19 · visual-art · Curator’s Immutable Log ===== Pitch: Pin curator notes, selections, and art metadata immutably to IPFS ensuring exhibition integrity. Page: https://arcprompts.lovable.app/ideas/visual-art-curator-s-immutable-log-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Curator’s Immutable Log" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin curator notes, selections, and art metadata immutably to IPFS ensuring exhibition integrity. Discipline: Visual Art (art exhibition curation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT ensures curator data permanence and tamper resistance 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CuratorsImmutableLog.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentCuratorsImmutableLog /// @notice Pin curator notes, selections, and art metadata immutably to IPFS ensuring exhibition integrity. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentCuratorsImmutableLog { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CuratorsImmutableLog` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CuratorsImmutableLog.sol CuratorsImmutableLog` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a art exhibition curation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-digital-canvas-ledger-4 · visual-art · Digital Canvas Ledger ===== Pitch: Record and pin painted artwork metadata to IPFS for permanent, tamper-proof provenance. Page: https://arcprompts.lovable.app/ideas/visual-art-digital-canvas-ledger-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Digital Canvas Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record and pin painted artwork metadata to IPFS for permanent, tamper-proof provenance. Discipline: Visual Art (painting provenance tracking). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s IPFS pinning ensures metadata immutability and easy retrieval for provenance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DigitalCanvasLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentDigitalCanvasLedger /// @notice Record and pin painted artwork metadata to IPFS for permanent, tamper-proof provenance. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentDigitalCanvasLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DigitalCanvasLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DigitalCanvasLedger.sol DigitalCanvasLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a painting provenance tracking, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-dynamic-exhibit-snapshot-24 · visual-art · Dynamic Exhibit Snapshot ===== Pitch: Pin snapshots of changing exhibitions and arrangements permanently for historical records. Page: https://arcprompts.lovable.app/ideas/visual-art-dynamic-exhibit-snapshot-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dynamic Exhibit Snapshot" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin snapshots of changing exhibitions and arrangements permanently for historical records. Discipline: Visual Art (exhibit state recording). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT allows frequent permanent pins of evolving JSON/image data on IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DynamicExhibitSnapshot.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentDynamicExhibitSnapshot /// @notice Pin snapshots of changing exhibitions and arrangements permanently for historical records. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentDynamicExhibitSnapshot { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DynamicExhibitSnapshot` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DynamicExhibitSnapshot.sol DynamicExhibitSnapshot` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a exhibit state recording, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-dynamic-frame-nfts-11 · visual-art · Dynamic Frame NFTs ===== Pitch: Create NFTs that change frames or borders based on onchain conditions or events. Page: https://arcprompts.lovable.app/ideas/visual-art-dynamic-frame-nfts-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dynamic Frame NFTs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create NFTs that change frames or borders based on onchain conditions or events. Discipline: Visual Art (programmable artwork framing). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enable dynamic metadata updates triggering visual changes in real-time. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DynamicFrameNFTs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DynamicFrameNFTs /// @notice Create NFTs that change frames or borders based on onchain conditions or events. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DynamicFrameNFTs { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DynamicFrameNFTs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DynamicFrameNFTs.sol DynamicFrameNFTs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a programmable artwork framing action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-edition-tracker-9 · visual-art · Edition Tracker ===== Pitch: Track edition numbers and print runs of limited artworks securely and transparently onchain. Page: https://arcprompts.lovable.app/ideas/visual-art-edition-tracker-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Edition Tracker" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track edition numbers and print runs of limited artworks securely and transparently onchain. Discipline: Visual Art (limited print tracking). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide immutable tracking of edition metadata and supply limits. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EditionTracker.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title EditionTracker /// @notice Track edition numbers and print runs of limited artworks securely and transparently onchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract EditionTracker { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EditionTracker` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EditionTracker.sol EditionTracker` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a limited print tracking action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-exhibit-chain-ledger-2 · visual-art · Exhibit Chain Ledger ===== Pitch: Record every artwork's exhibition history securely and transparently onchain for galleries and collectors. Page: https://arcprompts.lovable.app/ideas/visual-art-exhibit-chain-ledger-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Exhibit Chain Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Record every artwork's exhibition history securely and transparently onchain for galleries and collectors. Discipline: Visual Art (gallery exhibition tracking). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts provide decentralized, timestamped exhibition records. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ExhibitChainLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ExhibitChainLedger /// @notice Record every artwork's exhibition history securely and transparently onchain for galleries and collectors. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ExhibitChainLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ExhibitChainLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ExhibitChainLedger.sol ExhibitChainLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a gallery exhibition tracking action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-exhibit-echo-11 · visual-art · Exhibit Echo ===== Pitch: Preserve virtual gallery exhibits with creator-owned NFT provenance linked to IPFS. Page: https://arcprompts.lovable.app/ideas/visual-art-exhibit-echo-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Exhibit Echo" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Preserve virtual gallery exhibits with creator-owned NFT provenance linked to IPFS. Discipline: Visual Art (virtual exhibition records). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Immutable NFT minting captures and certifies virtual exhibit provenance onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ExhibitEcho.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ExhibitEcho /// @notice ERC-721 provenance for: Preserve virtual gallery exhibits with creator-owned NFT provenance linked to IPFS. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ExhibitEcho is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ExhibitEcho", "EXHIBI") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ExhibitEcho` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ExhibitEcho.sol ExhibitEcho` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a virtual exhibition records, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-exhibit-provenance-chain-7 · visual-art · Exhibit Provenance Chain ===== Pitch: Pin exhibit documents and artwork proofs to guarantee authenticity for global exhibitions. Page: https://arcprompts.lovable.app/ideas/visual-art-exhibit-provenance-chain-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Exhibit Provenance Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin exhibit documents and artwork proofs to guarantee authenticity for global exhibitions. Discipline: Visual Art (art exposition verification). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata IPFS storage offers permanent, censorship-resistant verification of exhibit data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ExhibitProvenanceChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentExhibitProvenanceChain /// @notice Pin exhibit documents and artwork proofs to guarantee authenticity for global exhibitions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentExhibitProvenanceChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ExhibitProvenanceChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ExhibitProvenanceChain.sol ExhibitProvenanceChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a art exposition verification, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-framechain-7 · visual-art · FrameChain ===== Pitch: Control and verify reproduction rights of artworks via mintable provenance NFTs. Page: https://arcprompts.lovable.app/ideas/visual-art-framechain-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FrameChain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Control and verify reproduction rights of artworks via mintable provenance NFTs. Discipline: Visual Art (art reproduction control). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 provenance tokens enable robust reproduction right tracking onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FrameChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title FrameChain /// @notice ERC-721 provenance for: Control and verify reproduction rights of artworks via mintable provenance NFTs. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FrameChain is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("FrameChain", "FRAMEC") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FrameChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FrameChain.sol FrameChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a art reproduction control, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-framefi-marketplace-11 · visual-art · FrameFi Marketplace ===== Pitch: Enable illustrators to sell artworks with embedded gasless framing add-ons and easy wallet login. Page: https://arcprompts.lovable.app/ideas/visual-art-framefi-marketplace-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FrameFi Marketplace" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable illustrators to sell artworks with embedded gasless framing add-ons and easy wallet login. Discipline: Visual Art (digital framing services). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet plus sponsored tx streamline gasless purchase of framing options. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FrameFiMarketplace.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogFrameFiMarketplace /// @notice Enable illustrators to sell artworks with embedded gasless framing add-ons and easy wallet login. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogFrameFiMarketplace { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FrameFiMarketplace` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FrameFiMarketplace.sol FrameFiMarketplace` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every digital framing services action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-gallery-access-token-12 · visual-art · Gallery Access Token ===== Pitch: Issue access tokens for exclusive gallery events and exhibitions secured on blockchain. Page: https://arcprompts.lovable.app/ideas/visual-art-gallery-access-token-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gallery Access Token" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Issue access tokens for exclusive gallery events and exhibitions secured on blockchain. Discipline: Visual Art (event admission tokens). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide verifiable, non-transferable access control tokens onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GalleryAccessToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title GalleryAccessToken /// @notice Issue access tokens for exclusive gallery events and exhibitions secured on blockchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract GalleryAccessToken { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GalleryAccessToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GalleryAccessToken.sol GalleryAccessToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a event admission tokens action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-gallery-gatekeeper-3 · visual-art · Gallery Gatekeeper ===== Pitch: Enable galleries to mint and verify provenance of physical artwork displayed or sold. Page: https://arcprompts.lovable.app/ideas/visual-art-gallery-gatekeeper-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gallery Gatekeeper" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable galleries to mint and verify provenance of physical artwork displayed or sold. Discipline: Visual Art (gallery asset curation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens provide immutable proof of artwork origin and gallery authentication. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GalleryGatekeeper.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title GalleryGatekeeper /// @notice ERC-721 provenance for: Enable galleries to mint and verify provenance of physical artwork displayed or sold. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract GalleryGatekeeper is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("GalleryGatekeeper", "GALLER") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GalleryGatekeeper` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GalleryGatekeeper.sol GalleryGatekeeper` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a gallery asset curation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-gallery-ghost-15 · visual-art · Gallery Ghost ===== Pitch: Gallerists create virtual galleries with gasless onchain ticketing and Google-based wallet onboarding. Page: https://arcprompts.lovable.app/ideas/visual-art-gallery-ghost-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gallery Ghost" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Gallerists create virtual galleries with gasless onchain ticketing and Google-based wallet onboarding. Discipline: Visual Art (virtual gallery hosting). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet plus sponsored tx enable cost-free event participation. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GalleryGhost.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGalleryGhost /// @notice Gallerists create virtual galleries with gasless onchain ticketing and Google-based wallet onboarding. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGalleryGhost { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GalleryGhost` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GalleryGhost.sol GalleryGhost` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every virtual gallery hosting action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-gallery-ledger-3 · visual-art · Gallery Ledger ===== Pitch: Provide gallerists with gasless, onchain tools to prove artwork provenance and authenticity effortlessly. Page: https://arcprompts.lovable.app/ideas/visual-art-gallery-ledger-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gallery Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Provide gallerists with gasless, onchain tools to prove artwork provenance and authenticity effortlessly. Discipline: Visual Art (art provenance tracking). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy-enabled social login and sponsored tx simplify provenance updates without gas costs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GalleryLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGalleryLedger /// @notice Provide gallerists with gasless, onchain tools to prove artwork provenance and authenticity effortlessly. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGalleryLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GalleryLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GalleryLedger.sol GalleryLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every art provenance tracking action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-gallery-manifest-hub-2 · visual-art · Gallery Manifest Hub ===== Pitch: Create and pin permanent digital manifests for art gallery exhibitions with immutable records. Page: https://arcprompts.lovable.app/ideas/visual-art-gallery-manifest-hub-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gallery Manifest Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create and pin permanent digital manifests for art gallery exhibitions with immutable records. Discipline: Visual Art (exhibition cataloging). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata enables reliable, permanent IPFS pinning of JSON manifests for galleries’ provenance needs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GalleryManifestHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentGalleryManifestHub /// @notice Create and pin permanent digital manifests for art gallery exhibitions with immutable records. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentGalleryManifestHub { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GalleryManifestHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GalleryManifestHub.sol GalleryManifestHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a exhibition cataloging, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-gallery-provenance-21 · visual-art · Gallery Provenance ===== Pitch: Digitally certify artwork origin and gallery exhibition history onchain with NFTs. Page: https://arcprompts.lovable.app/ideas/visual-art-gallery-provenance-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Gallery Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Digitally certify artwork origin and gallery exhibition history onchain with NFTs. Discipline: Visual Art (physical gallery tracking). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance mint ensures transparent gallery provenance authenticated on Sepolia. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GalleryProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title GalleryProvenance /// @notice ERC-721 provenance for: Digitally certify artwork origin and gallery exhibition history onchain with NFTs. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract GalleryProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("GalleryProvenance", "GALLER") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GalleryProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GalleryProvenance.sol GalleryProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a physical gallery tracking, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-generative-art-vault-1 · visual-art · Generative Art Vault ===== Pitch: Pin generative art scripts and outputs permanently to preserve creative provenance. Page: https://arcprompts.lovable.app/ideas/visual-art-generative-art-vault-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Generative Art Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin generative art scripts and outputs permanently to preserve creative provenance. Discipline: Visual Art (algorithmic artwork storage). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS via Pinata guarantees unalterable storage of code and images with CID verification. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GenerativeArtVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentGenerativeArtVault /// @notice Pin generative art scripts and outputs permanently to preserve creative provenance. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentGenerativeArtVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GenerativeArtVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GenerativeArtVault.sol GenerativeArtVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a algorithmic artwork storage, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-generative-canvas-2 · visual-art · Generative Canvas ===== Pitch: Let generative artists deploy and monetize code-driven art with instant gasless onboarding and transactions. Page: https://arcprompts.lovable.app/ideas/visual-art-generative-canvas-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Generative Canvas" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Let generative artists deploy and monetize code-driven art with instant gasless onboarding and transactions. Discipline: Visual Art (generative art). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored tx plus embedded wallet offer frictionless deployment and sales for generative artists. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GenerativeCanvas.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGenerativeCanvas /// @notice Let generative artists deploy and monetize code-driven art with instant gasless onboarding and transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGenerativeCanvas { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GenerativeCanvas` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GenerativeCanvas.sol GenerativeCanvas` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every generative art action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-generative-gallery-22 · visual-art · Generative Gallery ===== Pitch: Curate generative art exhibits featuring gasless ticketing and social login onboarding. Page: https://arcprompts.lovable.app/ideas/visual-art-generative-gallery-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Generative Gallery" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Curate generative art exhibits featuring gasless ticketing and social login onboarding. Discipline: Visual Art (algorithmic art exhibition). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet and sponsored transactions facilitate easy event access without fees. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GenerativeGallery.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogGenerativeGallery /// @notice Curate generative art exhibits featuring gasless ticketing and social login onboarding. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogGenerativeGallery { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GenerativeGallery` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GenerativeGallery.sol GenerativeGallery` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every algorithmic art exhibition action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-generative-genesis-2 · visual-art · Generative Genesis ===== Pitch: Track and mint each generative art iteration as unique collectible tokens. Page: https://arcprompts.lovable.app/ideas/visual-art-generative-genesis-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Generative Genesis" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track and mint each generative art iteration as unique collectible tokens. Discipline: Visual Art (generative art iterations). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Onchain NFTs securely link generative code outputs with IPFS CIDs for true provenance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GenerativeGenesis.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title GenerativeGenesis /// @notice ERC-721 provenance for: Track and mint each generative art iteration as unique collectible tokens. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract GenerativeGenesis is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("GenerativeGenesis", "GENERA") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GenerativeGenesis` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GenerativeGenesis.sol GenerativeGenesis` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a generative art iterations, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-generative-mint-studio-1 · visual-art · Generative Mint Studio ===== Pitch: Artists can mint unique generative art pieces directly onto Ethereum Sepolia with automated smart contracts. Page: https://arcprompts.lovable.app/ideas/visual-art-generative-mint-studio-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Generative Mint Studio" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Artists can mint unique generative art pieces directly onto Ethereum Sepolia with automated smart contracts. Discipline: Visual Art (generative art minting). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enable onchain minting with verifiable randomness and ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GenerativeMintStudio.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title GenerativeMintStudio /// @notice Artists can mint unique generative art pieces directly onto Ethereum Sepolia with automated smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract GenerativeMintStudio { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GenerativeMintStudio` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GenerativeMintStudio.sol GenerativeMintStudio` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a generative art minting action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-generative-token-gallery-14 · visual-art · Generative Token Gallery ===== Pitch: Pin generative art manifests and images for permanent token-backed gallery display. Page: https://arcprompts.lovable.app/ideas/visual-art-generative-token-gallery-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Generative Token Gallery" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin generative art manifests and images for permanent token-backed gallery display. Discipline: Visual Art (tokenized generative art). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata IPFS storage ensures consistent art data linked to blockchain tokens forever. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/GenerativeTokenGallery.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentGenerativeTokenGallery /// @notice Pin generative art manifests and images for permanent token-backed gallery display. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentGenerativeTokenGallery { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs GenerativeTokenGallery` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/GenerativeTokenGallery.sol GenerativeTokenGallery` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a tokenized generative art, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-illustrate-impact-19 · visual-art · Illustrate Impact ===== Pitch: Facilitate gasless donations and sponsorships for illustrators supporting social causes via easy login. Page: https://arcprompts.lovable.app/ideas/visual-art-illustrate-impact-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Illustrate Impact" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Facilitate gasless donations and sponsorships for illustrators supporting social causes via easy login. Discipline: Visual Art (art philanthropy). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social and sponsored tx ensure smooth, zero-fee charitable transactions. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/IllustrateImpact.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogIllustrateImpact /// @notice Facilitate gasless donations and sponsorships for illustrators supporting social causes via easy login. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogIllustrateImpact { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs IllustrateImpact` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/IllustrateImpact.sol IllustrateImpact` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every art philanthropy action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-illustration-rights-ledger-15 · visual-art · Illustration Rights Ledger ===== Pitch: Pin illustration licenses and usage terms permanently for transparent rights management. Page: https://arcprompts.lovable.app/ideas/visual-art-illustration-rights-ledger-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Illustration Rights Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin illustration licenses and usage terms permanently for transparent rights management. Discipline: Visual Art (digital rights management). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s immutable IPFS pins secure rights documents for easy verification. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/IllustrationRightsLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentIllustrationRightsLedger /// @notice Pin illustration licenses and usage terms permanently for transparent rights management. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentIllustrationRightsLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs IllustrationRightsLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/IllustrationRightsLedger.sol IllustrationRightsLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a digital rights management, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-illustrator-guild-1 · visual-art · Illustrator Guild ===== Pitch: Create a hosted network for illustrators to share, sell, and verify work with easy login and zero gas fees. Page: https://arcprompts.lovable.app/ideas/visual-art-illustrator-guild-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Illustrator Guild" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create a hosted network for illustrators to share, sell, and verify work with easy login and zero gas fees. Discipline: Visual Art (illustrator community). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet with Google sign-in removes blockchain barriers for illustrators expanding their market. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/IllustratorGuild.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogIllustratorGuild /// @notice Create a hosted network for illustrators to share, sell, and verify work with easy login and zero gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogIllustratorGuild { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs IllustratorGuild` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/IllustratorGuild.sol IllustratorGuild` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every illustrator community action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-illustrator-ink-21 · visual-art · Illustrator Ink ===== Pitch: Support ink artists selling limited editions with gasless minting and sponsored transaction flows. Page: https://arcprompts.lovable.app/ideas/visual-art-illustrator-ink-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Illustrator Ink" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Support ink artists selling limited editions with gasless minting and sponsored transaction flows. Discipline: Visual Art (digital ink art). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet plus sponsored tx create seamless gasless mint experiences. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/IllustratorInk.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogIllustratorInk /// @notice Support ink artists selling limited editions with gasless minting and sponsored transaction flows. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogIllustratorInk { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs IllustratorInk` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/IllustratorInk.sol IllustratorInk` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every digital ink art action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-illustrator-portfolio-lock-3 · visual-art · Illustrator Portfolio Lock ===== Pitch: Permanently store and share your illustrator portfolios with verifiable IPFS links. Page: https://arcprompts.lovable.app/ideas/visual-art-illustrator-portfolio-lock-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Illustrator Portfolio Lock" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Permanently store and share your illustrator portfolios with verifiable IPFS links. Discipline: Visual Art (portfolio archiving). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT simplifies secure uploading and pinning with permanent CID references. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/IllustratorPortfolioLock.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentIllustratorPortfolioLock /// @notice Permanently store and share your illustrator portfolios with verifiable IPFS links. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentIllustratorPortfolioLock { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs IllustratorPortfolioLock` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/IllustratorPortfolioLock.sol IllustratorPortfolioLock` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a portfolio archiving, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-illustrator-s-imprint-8 · visual-art · Illustrator’s Imprint ===== Pitch: Mint signed digital prints with verifiable creator provenance directly on the blockchain. Page: https://arcprompts.lovable.app/ideas/visual-art-illustrator-s-imprint-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Illustrator’s Imprint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint signed digital prints with verifiable creator provenance directly on the blockchain. Discipline: Visual Art (signed digital prints). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens link signed print IPFS CIDs to creator ownership proofs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/IllustratorsImprint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title IllustratorsImprint /// @notice ERC-721 provenance for: Mint signed digital prints with verifiable creator provenance directly on the blockchain. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract IllustratorsImprint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("IllustratorsImprint", "ILLUST") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs IllustratorsImprint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/IllustratorsImprint.sol IllustratorsImprint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a signed digital prints, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-interactive-art-pinning-6 · visual-art · Interactive Art Pinning ===== Pitch: Pin interactive art assets as JSON and media files, preserving experience forever on IPFS. Page: https://arcprompts.lovable.app/ideas/visual-art-interactive-art-pinning-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Interactive Art Pinning" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin interactive art assets as JSON and media files, preserving experience forever on IPFS. Discipline: Visual Art (multimedia art preservation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s JWT upload supports complex JSON and media pinning for interaction fidelity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InteractiveArtPinning.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentInteractiveArtPinning /// @notice Pin interactive art assets as JSON and media files, preserving experience forever on IPFS. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentInteractiveArtPinning { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InteractiveArtPinning` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InteractiveArtPinning.sol InteractiveArtPinning` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a multimedia art preservation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-interactive-auction-game-15 · visual-art · Interactive Auction Game ===== Pitch: Turn art auctions into engaging games with transparent rules enforced by onchain smart contracts. Page: https://arcprompts.lovable.app/ideas/visual-art-interactive-auction-game-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Interactive Auction Game" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Turn art auctions into engaging games with transparent rules enforced by onchain smart contracts. Discipline: Visual Art (gamified art auctions). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enable trustless, rule-based game mechanics for auctions. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InteractiveAuctionGame.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title InteractiveAuctionGame /// @notice Turn art auctions into engaging games with transparent rules enforced by onchain smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract InteractiveAuctionGame { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InteractiveAuctionGame` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InteractiveAuctionGame.sol InteractiveAuctionGame` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a gamified art auctions action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-interactive-canvas-dao-7 · visual-art · Interactive Canvas DAO ===== Pitch: Enable collective decision-making on collaborative visual art projects via decentralized onchain voting. Page: https://arcprompts.lovable.app/ideas/visual-art-interactive-canvas-dao-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Interactive Canvas DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable collective decision-making on collaborative visual art projects via decentralized onchain voting. Discipline: Visual Art (collaborative art governance). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts support secure DAO governance with transparent vote tallying. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/InteractiveCanvasDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title InteractiveCanvasDAO /// @notice Enable collective decision-making on collaborative visual art projects via decentralized onchain voting. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract InteractiveCanvasDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs InteractiveCanvasDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/InteractiveCanvasDAO.sol InteractiveCanvasDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a collaborative art governance action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-layer-ledger-14 · visual-art · Layer Ledger ===== Pitch: Track and mint individual painting layers proving creative process and ownership. Page: https://arcprompts.lovable.app/ideas/visual-art-layer-ledger-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Layer Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track and mint individual painting layers proving creative process and ownership. Discipline: Visual Art (digital painting layers). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 minting uniquely links layered IPFS CIDs with artist ownership proofs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LayerLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title LayerLedger /// @notice ERC-721 provenance for: Track and mint individual painting layers proving creative process and ownership. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract LayerLedger is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("LayerLedger", "LAYERL") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LayerLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LayerLedger.sol LayerLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a digital painting layers, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-licensing-ledger-6 · visual-art · Licensing Ledger ===== Pitch: Manage and enforce artwork licensing rights transparently with blockchain smart contracts. Page: https://arcprompts.lovable.app/ideas/visual-art-licensing-ledger-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Licensing Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Manage and enforce artwork licensing rights transparently with blockchain smart contracts. Discipline: Visual Art (artwork license management). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts automate license terms and enforce royalties securely onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LicensingLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title LicensingLedger /// @notice Manage and enforce artwork licensing rights transparently with blockchain smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract LicensingLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LicensingLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LicensingLedger.sol LicensingLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a artwork license management action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-masterstroke-mint-13 · visual-art · Masterstroke Mint ===== Pitch: Create immutable certificates of authenticity for signature artworks via NFT provenance mint. Page: https://arcprompts.lovable.app/ideas/visual-art-masterstroke-mint-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Masterstroke Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create immutable certificates of authenticity for signature artworks via NFT provenance mint. Discipline: Visual Art (signature artwork certs). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance ensures unforgeable certification linked to IPFS-stored artwork data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MasterstrokeMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MasterstrokeMint /// @notice ERC-721 provenance for: Create immutable certificates of authenticity for signature artworks via NFT provenance mint. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MasterstrokeMint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MasterstrokeMint", "MASTER") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MasterstrokeMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MasterstrokeMint.sol MasterstrokeMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a signature artwork certs, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-material-usage-ledger-16 · visual-art · Material Usage Ledger ===== Pitch: Track and verify the ethical sourcing and usage of materials in visual artworks onchain. Page: https://arcprompts.lovable.app/ideas/visual-art-material-usage-ledger-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Material Usage Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track and verify the ethical sourcing and usage of materials in visual artworks onchain. Discipline: Visual Art (supply chain transparency). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide immutable supply chain transparency for art materials. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MaterialUsageLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title MaterialUsageLedger /// @notice Track and verify the ethical sourcing and usage of materials in visual artworks onchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MaterialUsageLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MaterialUsageLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MaterialUsageLedger.sol MaterialUsageLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a supply chain transparency action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-minted-murals-6 · visual-art · Minted Murals ===== Pitch: Authenticate and immortalize public murals with creator-owned NFT provenance tokens. Page: https://arcprompts.lovable.app/ideas/visual-art-minted-murals-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Minted Murals" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate and immortalize public murals with creator-owned NFT provenance tokens. Discipline: Visual Art (public mural documentation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Immutable onchain minting guarantees provenance and long-term mural authenticity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MintedMurals.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MintedMurals /// @notice ERC-721 provenance for: Authenticate and immortalize public murals with creator-owned NFT provenance tokens. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MintedMurals is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MintedMurals", "MINTED") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MintedMurals` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MintedMurals.sol MintedMurals` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a public mural documentation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-moodboard-dao-18 · visual-art · MoodBoard DAO ===== Pitch: Artists collaboratively curate and govern mood boards for projects using decentralized governance. Page: https://arcprompts.lovable.app/ideas/visual-art-moodboard-dao-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MoodBoard DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Artists collaboratively curate and govern mood boards for projects using decentralized governance. Discipline: Visual Art (collective inspiration curation). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts facilitate transparent proposal voting and curation rewards. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MoodBoardDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title MoodBoardDAO /// @notice Artists collaboratively curate and govern mood boards for projects using decentralized governance. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MoodBoardDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MoodBoardDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MoodBoardDAO.sol MoodBoardDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a collective inspiration curation action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-motion-mint-23 · visual-art · Motion Mint ===== Pitch: Mint creator-owned NFTs for animated illustrations backed by IPFS-stored assets. Page: https://arcprompts.lovable.app/ideas/visual-art-motion-mint-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Motion Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint creator-owned NFTs for animated illustrations backed by IPFS-stored assets. Discipline: Visual Art (animated illustration ownership). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 provenance mint guarantees creator control and provenance of animation files. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MotionMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MotionMint /// @notice ERC-721 provenance for: Mint creator-owned NFTs for animated illustrations backed by IPFS-stored assets. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MotionMint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MotionMint", "MOTION") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MotionMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MotionMint.sol MotionMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a animated illustration ownership, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-mural-mesh-12 · visual-art · Mural Mesh ===== Pitch: Coordinate large-scale public murals with painters via gasless onchain collaboration and social login. Page: https://arcprompts.lovable.app/ideas/visual-art-mural-mesh-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Mural Mesh" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Coordinate large-scale public murals with painters via gasless onchain collaboration and social login. Discipline: Visual Art (public mural collaboration). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy sponsored transactions ease multi-party approval without gas costs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MuralMesh.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogMuralMesh /// @notice Coordinate large-scale public murals with painters via gasless onchain collaboration and social login. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogMuralMesh { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MuralMesh` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MuralMesh.sol MuralMesh` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every public mural collaboration action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-nft-colorgram-22 · visual-art · NFT Colorgram ===== Pitch: Create NFTs representing exclusive color-themed artworks with immutable provenance. Page: https://arcprompts.lovable.app/ideas/visual-art-nft-colorgram-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NFT Colorgram" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create NFTs representing exclusive color-themed artworks with immutable provenance. Discipline: Visual Art (chromatic art NFTs). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 mint links color-centric art stored on IPFS with verified creator proof. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NFTColorgram.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title NFTColorgram /// @notice ERC-721 provenance for: Create NFTs representing exclusive color-themed artworks with immutable provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract NFTColorgram is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("NFTColorgram", "NFTCOL") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NFTColorgram` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NFTColorgram.sol NFTColorgram` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a chromatic art NFTs, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-nft-curation-index-21 · visual-art · NFT Curation Index ===== Pitch: Create and manage curated NFT visual art collections with transparent onchain voting and tracking. Page: https://arcprompts.lovable.app/ideas/visual-art-nft-curation-index-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NFT Curation Index" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create and manage curated NFT visual art collections with transparent onchain voting and tracking. Discipline: Visual Art (curated NFT collections). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enable decentralized curation and provenance validation. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NFTCurationIndex.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title NFTCurationIndex /// @notice Create and manage curated NFT visual art collections with transparent onchain voting and tracking. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract NFTCurationIndex { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NFTCurationIndex` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NFTCurationIndex.sol NFTCurationIndex` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a curated NFT collections action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-nft-manifest-generator-5 · visual-art · NFT Manifest Generator ===== Pitch: Bundle images and metadata into permanent IPFS manifests to guarantee NFT authenticity. Page: https://arcprompts.lovable.app/ideas/visual-art-nft-manifest-generator-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NFT Manifest Generator" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Bundle images and metadata into permanent IPFS manifests to guarantee NFT authenticity. Discipline: Visual Art (digital asset packaging). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT provides seamless and permanent IPFS pinning ideal for NFT data integrity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NFTManifestGenerator.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentNFTManifestGenerator /// @notice Bundle images and metadata into permanent IPFS manifests to guarantee NFT authenticity. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentNFTManifestGenerator { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NFTManifestGenerator` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NFTManifestGenerator.sol NFTManifestGenerator` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a digital asset packaging, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-nft-mirror-19 · visual-art · NFT Mirror ===== Pitch: Mint paired NFTs representing original and reflected artworks to showcase creative duality. Page: https://arcprompts.lovable.app/ideas/visual-art-nft-mirror-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NFT Mirror" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint paired NFTs representing original and reflected artworks to showcase creative duality. Discipline: Visual Art (artistic reflection sets). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens prove paired provenance anchored to immutable IPFS data sets. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NFTMirror.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title NFTMirror /// @notice ERC-721 provenance for: Mint paired NFTs representing original and reflected artworks to showcase creative duality. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract NFTMirror is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("NFTMirror", "NFTMIR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NFTMirror` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NFTMirror.sol NFTMirror` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a artistic reflection sets, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-nft-splitter-6 · visual-art · NFT Splitter ===== Pitch: Enable painters and collectors to split artwork ownership onchain with easy social login and no gas fees. Page: https://arcprompts.lovable.app/ideas/visual-art-nft-splitter-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NFT Splitter" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable painters and collectors to split artwork ownership onchain with easy social login and no gas fees. Discipline: Visual Art (fractional art ownership). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored tx make fractional ownership seamless without transaction costs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NFTSplitter.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogNFTSplitter /// @notice Enable painters and collectors to split artwork ownership onchain with easy social login and no gas fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogNFTSplitter { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NFTSplitter` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NFTSplitter.sol NFTSplitter` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every fractional art ownership action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-painter-s-time-capsule-8 · visual-art · Painter’s Time Capsule ===== Pitch: Permanently archive each painting stage as images and notes pinned immutably on IPFS. Page: https://arcprompts.lovable.app/ideas/visual-art-painter-s-time-capsule-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Painter’s Time Capsule" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Permanently archive each painting stage as images and notes pinned immutably on IPFS. Discipline: Visual Art (artistic process documentation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT enables secure sequential uploads to preserve process documentation forever. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PaintersTimeCapsule.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPaintersTimeCapsule /// @notice Permanently archive each painting stage as images and notes pinned immutably on IPFS. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPaintersTimeCapsule { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PaintersTimeCapsule` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PaintersTimeCapsule.sol PaintersTimeCapsule` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a artistic process documentation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-palette-swap-4 · visual-art · Palette Swap ===== Pitch: Allow painters and illustrators to exchange and license color palettes with transparent, gasless transactions. Page: https://arcprompts.lovable.app/ideas/visual-art-palette-swap-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Palette Swap" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Allow painters and illustrators to exchange and license color palettes with transparent, gasless transactions. Discipline: Visual Art (color exchange). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored transactions enable easy palette licensing with zero gas hurdles. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PaletteSwap.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPaletteSwap /// @notice Allow painters and illustrators to exchange and license color palettes with transparent, gasless transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPaletteSwap { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PaletteSwap` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PaletteSwap.sol PaletteSwap` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every color exchange action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-permacolor-archive-0 · visual-art · PermaColor Archive ===== Pitch: Secure and share your unique color palettes as permanent IPFS references. Page: https://arcprompts.lovable.app/ideas/visual-art-permacolor-archive-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PermaColor Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Secure and share your unique color palettes as permanent IPFS references. Discipline: Visual Art (color palette curation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT ensures immutable storage and easy retrieval of palette metadata on IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PermaColorArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPermaColorArchive /// @notice Secure and share your unique color palettes as permanent IPFS references. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPermaColorArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PermaColorArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PermaColorArchive.sol PermaColorArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a color palette curation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-permanent-sketchbook-hub-17 · visual-art · Permanent Sketchbook Hub ===== Pitch: Pin your sketchbook pages as immutable IPFS entries to build a lifelong visual diary. Page: https://arcprompts.lovable.app/ideas/visual-art-permanent-sketchbook-hub-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Permanent Sketchbook Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin your sketchbook pages as immutable IPFS entries to build a lifelong visual diary. Discipline: Visual Art (digital sketch archiving). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT allows easy permanent uploads ensuring sketches remain unchanged and accessible. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PermanentSketchbookHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPermanentSketchbookHub /// @notice Pin your sketchbook pages as immutable IPFS entries to build a lifelong visual diary. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPermanentSketchbookHub { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PermanentSketchbookHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PermanentSketchbookHub.sol PermanentSketchbookHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a digital sketch archiving, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-pixel-provenance-10 · visual-art · Pixel Provenance ===== Pitch: Securely mint pixel art pieces with immutable creator provenance and IPFS metadata. Page: https://arcprompts.lovable.app/ideas/visual-art-pixel-provenance-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Pixel Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely mint pixel art pieces with immutable creator provenance and IPFS metadata. Discipline: Visual Art (pixel art authentication). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 on Sepolia anchors pixel art provenance for collectors and artists alike. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PixelProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PixelProvenance /// @notice ERC-721 provenance for: Securely mint pixel art pieces with immutable creator provenance and IPFS metadata. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PixelProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PixelProvenance", "PIXELP") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PixelProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PixelProvenance.sol PixelProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a pixel art authentication, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-provenance-palette-0 · visual-art · Provenance Palette ===== Pitch: Securely track and prove the ownership lineage of paintings with tamper-proof blockchain records. Page: https://arcprompts.lovable.app/ideas/visual-art-provenance-palette-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Provenance Palette" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely track and prove the ownership lineage of paintings with tamper-proof blockchain records. Discipline: Visual Art (art ownership tracking). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts provide immutable provenance records verified 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ProvenancePalette.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ProvenancePalette /// @notice Securely track and prove the ownership lineage of paintings with tamper-proof blockchain records. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ProvenancePalette { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ProvenancePalette` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ProvenancePalette.sol ProvenancePalette` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a art ownership tracking action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-provenance-palette-24 · visual-art · Provenance Palette ===== Pitch: Digitize and mint physical palette images as NFTs preserving unique artist color mixes. Page: https://arcprompts.lovable.app/ideas/visual-art-provenance-palette-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Provenance Palette" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Digitize and mint physical palette images as NFTs preserving unique artist color mixes. Discipline: Visual Art (physical palette digitization). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Immutable NFT mint authenticates physical palette provenance via IPFS hashes. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ProvenancePalette.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ProvenancePalette /// @notice ERC-721 provenance for: Digitize and mint physical palette images as NFTs preserving unique artist color mixes. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ProvenancePalette is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ProvenancePalette", "PROVEN") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ProvenancePalette` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ProvenancePalette.sol ProvenancePalette` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a physical palette digitization, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-provenance-palette-9 · visual-art · Provenance Palette ===== Pitch: Digitally certify and mint provenance tokens for unique physical artist palettes. Page: https://arcprompts.lovable.app/ideas/visual-art-provenance-palette-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Provenance Palette" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Digitally certify and mint provenance tokens for unique physical artist palettes. Discipline: Visual Art (artist palette provenance). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT minting onchain ensures unalterable provenance linked to physical artifacts. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ProvenancePalette.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ProvenancePalette /// @notice ERC-721 provenance for: Digitally certify and mint provenance tokens for unique physical artist palettes. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ProvenancePalette is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ProvenancePalette", "PROVEN") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ProvenancePalette` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ProvenancePalette.sol ProvenancePalette` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a artist palette provenance, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-signature-seal-18 · visual-art · Signature Seal ===== Pitch: Allow artists to mint unique digital signatures as NFT proof of authorship and authenticity. Page: https://arcprompts.lovable.app/ideas/visual-art-signature-seal-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Signature Seal" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Allow artists to mint unique digital signatures as NFT proof of authorship and authenticity. Discipline: Visual Art (digital signature minting). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: Immutable NFT mint links digital signature to IPFS asset verifiably onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SignatureSeal.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SignatureSeal /// @notice ERC-721 provenance for: Allow artists to mint unique digital signatures as NFT proof of authorship and authenticity. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SignatureSeal is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SignatureSeal", "SIGNAT") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SignatureSeal` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SignatureSeal.sol SignatureSeal` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a digital signature minting, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-signature-stamp-14 · visual-art · Signature Stamp ===== Pitch: Cryptographically verify artist signatures on digital and physical artworks using blockchain stamps. Page: https://arcprompts.lovable.app/ideas/visual-art-signature-stamp-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Signature Stamp" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Cryptographically verify artist signatures on digital and physical artworks using blockchain stamps. Discipline: Visual Art (artist signature authentication). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts store immutable signature proofs linked to artworks. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SignatureStamp.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SignatureStamp /// @notice Cryptographically verify artist signatures on digital and physical artworks using blockchain stamps. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SignatureStamp { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SignatureStamp` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SignatureStamp.sol SignatureStamp` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a artist signature authentication action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-sketch-snapshot-chain-19 · visual-art · Sketch Snapshot Chain ===== Pitch: Instantly archive and timestamp digital sketches onchain for secure creative journaling. Page: https://arcprompts.lovable.app/ideas/visual-art-sketch-snapshot-chain-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sketch Snapshot Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Instantly archive and timestamp digital sketches onchain for secure creative journaling. Discipline: Visual Art (digital sketch archiving). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts provide immutable and decentralized snapshot storage proofs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SketchSnapshotChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SketchSnapshotChain /// @notice Instantly archive and timestamp digital sketches onchain for secure creative journaling. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SketchSnapshotChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SketchSnapshotChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SketchSnapshotChain.sol SketchSnapshotChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a digital sketch archiving action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-sketch-stamp-1 · visual-art · Sketch Stamp ===== Pitch: Authenticate and timestamp early illustration drafts to prove originality and creative process. Page: https://arcprompts.lovable.app/ideas/visual-art-sketch-stamp-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sketch Stamp" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Authenticate and timestamp early illustration drafts to prove originality and creative process. Discipline: Visual Art (illustration drafts). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 mint links creators to IPFS-stored drafts ensuring provenance auditability. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SketchStamp.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SketchStamp /// @notice ERC-721 provenance for: Authenticate and timestamp early illustration drafts to prove originality and creative process. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SketchStamp is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SketchStamp", "SKETCH") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SketchStamp` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SketchStamp.sol SketchStamp` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a illustration drafts, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-sketchset-swap-17 · visual-art · SketchSet Swap ===== Pitch: Illustrators trade and license brush presets with zero gas fees and easy Google sign-in wallets. Page: https://arcprompts.lovable.app/ideas/visual-art-sketchset-swap-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SketchSet Swap" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Illustrators trade and license brush presets with zero gas fees and easy Google sign-in wallets. Discipline: Visual Art (brush preset exchange). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored transactions eliminate friction in preset licensing. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SketchSetSwap.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSketchSetSwap /// @notice Illustrators trade and license brush presets with zero gas fees and easy Google sign-in wallets. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSketchSetSwap { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SketchSetSwap` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SketchSetSwap.sol SketchSetSwap` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every brush preset exchange action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-sketchstream-5 · visual-art · SketchStream ===== Pitch: Stream live sketches with integrated onchain tips and gasless wallet logins for artist-audience engagement. Page: https://arcprompts.lovable.app/ideas/visual-art-sketchstream-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SketchStream" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Stream live sketches with integrated onchain tips and gasless wallet logins for artist-audience engagement. Discipline: Visual Art (live drawing streams). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet and sponsored tx allow instant, gas-free microtransactions during live art streams. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SketchStream.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSketchStream /// @notice Stream live sketches with integrated onchain tips and gasless wallet logins for artist-audience engagement. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSketchStream { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SketchStream` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SketchStream.sol SketchStream` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every live drawing streams action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-sketchvault-8 · visual-art · SketchVault ===== Pitch: Offer painters a gasless, onchain-secured vault for storing and sharing preliminary sketches with social login. Page: https://arcprompts.lovable.app/ideas/visual-art-sketchvault-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SketchVault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Offer painters a gasless, onchain-secured vault for storing and sharing preliminary sketches with social login. Discipline: Visual Art (secure sketch storage). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet plus sponsored tx enable secure, cost-free storage of sensitive creative drafts. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SketchVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogSketchVault /// @notice Offer painters a gasless, onchain-secured vault for storing and sharing preliminary sketches with social login. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogSketchVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SketchVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SketchVault.sol SketchVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every secure sketch storage action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-storyboard-stamp-12 · visual-art · Storyboard Stamp ===== Pitch: Mint sequential NFTs to authenticate evolving concept art storyboards and ideas. Page: https://arcprompts.lovable.app/ideas/visual-art-storyboard-stamp-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Storyboard Stamp" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint sequential NFTs to authenticate evolving concept art storyboards and ideas. Discipline: Visual Art (concept art progression). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens create immutable chains of concept art evolution on the blockchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StoryboardStamp.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title StoryboardStamp /// @notice ERC-721 provenance for: Mint sequential NFTs to authenticate evolving concept art storyboards and ideas. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract StoryboardStamp is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("StoryboardStamp", "STORYB") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StoryboardStamp` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StoryboardStamp.sol StoryboardStamp` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a concept art progression, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-style-transfer-keeper-12 · visual-art · Style Transfer Keeper ===== Pitch: Pin AI-generated style files and outputs permanently to safeguard unique artistic styles. Page: https://arcprompts.lovable.app/ideas/visual-art-style-transfer-keeper-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Style Transfer Keeper" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin AI-generated style files and outputs permanently to safeguard unique artistic styles. Discipline: Visual Art (AI style preservation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata supports permanent pinning of JSON styles and image outputs ensuring longevity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StyleTransferKeeper.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentStyleTransferKeeper /// @notice Pin AI-generated style files and outputs permanently to safeguard unique artistic styles. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentStyleTransferKeeper { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StyleTransferKeeper` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StyleTransferKeeper.sol StyleTransferKeeper` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a AI style preservation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-token-tapestry-14 · visual-art · Token Tapestry ===== Pitch: Artists tokenize and sell unique textile patterns with gasless wallet integration and sponsored transactions. Page: https://arcprompts.lovable.app/ideas/visual-art-token-tapestry-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Token Tapestry" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Artists tokenize and sell unique textile patterns with gasless wallet integration and sponsored transactions. Discipline: Visual Art (textile pattern art). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus sponsored tx ensure seamless gasless sales of tokenized patterns. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/TokenTapestry.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogTokenTapestry /// @notice Artists tokenize and sell unique textile patterns with gasless wallet integration and sponsored transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogTokenTapestry { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs TokenTapestry` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/TokenTapestry.sol TokenTapestry` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every textile pattern art action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-unique-print-provenance-21 · visual-art · Unique Print Provenance ===== Pitch: Pin limited print metadata and certificates immutably to prove authenticity for collectors. Page: https://arcprompts.lovable.app/ideas/visual-art-unique-print-provenance-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Unique Print Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin limited print metadata and certificates immutably to prove authenticity for collectors. Discipline: Visual Art (limited edition prints). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata IPFS pinning offers permanent certificate storage verified by CID. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/UniquePrintProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentUniquePrintProvenance /// @notice Pin limited print metadata and certificates immutably to prove authenticity for collectors. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentUniquePrintProvenance { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs UniquePrintProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/UniquePrintProvenance.sol UniquePrintProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a limited edition prints, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-visual-art-rewards-24 · visual-art · Visual Art Rewards ===== Pitch: Reward artists and illustrators for community engagement or milestones via onchain token incentives. Page: https://arcprompts.lovable.app/ideas/visual-art-visual-art-rewards-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Visual Art Rewards" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Reward artists and illustrators for community engagement or milestones via onchain token incentives. Discipline: Visual Art (artist incentivization). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts manage transparent and automated reward distributions. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VisualArtRewards.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title VisualArtRewards /// @notice Reward artists and illustrators for community engagement or milestones via onchain token incentives. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VisualArtRewards { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VisualArtRewards` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VisualArtRewards.sol VisualArtRewards` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a artist incentivization action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-visual-art-supply-chain-9 · visual-art · Visual Art Supply Chain ===== Pitch: Pin supplier certificates and provenance of materials to IPFS for buyer transparency. Page: https://arcprompts.lovable.app/ideas/visual-art-visual-art-supply-chain-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Visual Art Supply Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin supplier certificates and provenance of materials to IPFS for buyer transparency. Discipline: Visual Art (art materials tracking). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s permanent storage provides unalterable proof of authenticity for art supplies. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VisualArtSupplyChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentVisualArtSupplyChain /// @notice Pin supplier certificates and provenance of materials to IPFS for buyer transparency. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentVisualArtSupplyChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VisualArtSupplyChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VisualArtSupplyChain.sol VisualArtSupplyChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a art materials tracking, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-visual-remix-repository-20 · visual-art · Visual Remix Repository ===== Pitch: Pin remixed artworks and source attribution information permanently for remix transparency. Page: https://arcprompts.lovable.app/ideas/visual-art-visual-remix-repository-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Visual Remix Repository" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin remixed artworks and source attribution information permanently for remix transparency. Discipline: Visual Art (art remix documentation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata enables permanent IPFS storage linking source and new art metadata. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VisualRemixRepository.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentVisualRemixRepository /// @notice Pin remixed artworks and source attribution information permanently for remix transparency. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentVisualRemixRepository { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VisualRemixRepository` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VisualRemixRepository.sol VisualRemixRepository` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a art remix documentation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-visual-rights-archive-23 · visual-art · Visual Rights Archive ===== Pitch: Pin copyright registration data and proofs permanently for artists’ legal protection. Page: https://arcprompts.lovable.app/ideas/visual-art-visual-rights-archive-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Visual Rights Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin copyright registration data and proofs permanently for artists’ legal protection. Discipline: Visual Art (copyright preservation). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s permanent IPFS pinning guarantees copyright data immutability and accessibility. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VisualRightsArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentVisualRightsArchive /// @notice Pin copyright registration data and proofs permanently for artists’ legal protection. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentVisualRightsArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VisualRightsArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VisualRightsArchive.sol VisualRightsArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a copyright preservation, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-visual-royalties-manager-17 · visual-art · Visual Royalties Manager ===== Pitch: Automate royalty payments to artists via smart contracts every time artwork changes hands. Page: https://arcprompts.lovable.app/ideas/visual-art-visual-royalties-manager-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Visual Royalties Manager" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Automate royalty payments to artists via smart contracts every time artwork changes hands. Discipline: Visual Art (artist royalty automation). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enforce automatic royalty splits transparently and reliably. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VisualRoyaltiesManager.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title VisualRoyaltiesManager /// @notice Automate royalty payments to artists via smart contracts every time artwork changes hands. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VisualRoyaltiesManager { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VisualRoyaltiesManager` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VisualRoyaltiesManager.sol VisualRoyaltiesManager` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a artist royalty automation action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA visual-art-visual-storyboard-chain-18 · visual-art · Visual Storyboard Chain ===== Pitch: Pin storyboards and frame metadata permanently for narrative visual art projects. Page: https://arcprompts.lovable.app/ideas/visual-art-visual-storyboard-chain-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Visual Storyboard Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin storyboards and frame metadata permanently for narrative visual art projects. Discipline: Visual Art (narrative art sequencing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s JSON and image pinning preserves sequence integrity on IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VisualStoryboardChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentVisualStoryboardChain /// @notice Pin storyboards and frame metadata permanently for narrative visual art projects. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentVisualStoryboardChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VisualStoryboardChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VisualStoryboardChain.sol VisualStoryboardChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a narrative art sequencing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-authorshipchain-9 · writing · AuthorshipChain ===== Pitch: Manage and verify author identities and pseudonyms securely through smart contract registries. Page: https://arcprompts.lovable.app/ideas/writing-authorshipchain-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "AuthorshipChain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Manage and verify author identities and pseudonyms securely through smart contract registries. Discipline: Writing, Poetry & Narrative (writer identity). Onchain primitive: Arc smart contract. Why this primitive: Onchain identity contracts prevent impersonation and ensure author authenticity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/AuthorshipChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title AuthorshipChain /// @notice Manage and verify author identities and pseudonyms securely through smart contract registries. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract AuthorshipChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs AuthorshipChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/AuthorshipChain.sol AuthorshipChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a writer identity action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-character-bios-hub-2 · writing · Character Bios Hub ===== Pitch: Store and distribute rich, immutable character profiles with multimedia attachments. Page: https://arcprompts.lovable.app/ideas/writing-character-bios-hub-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Character Bios Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store and distribute rich, immutable character profiles with multimedia attachments. Discipline: Writing, Poetry & Narrative (character development). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT upload to IPFS supports multi-format and permanent character asset pinning. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CharacterBiosHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentCharacterBiosHub /// @notice Store and distribute rich, immutable character profiles with multimedia attachments. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentCharacterBiosHub { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CharacterBiosHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CharacterBiosHub.sol CharacterBiosHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a character development, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-characternft-18 · writing · CharacterNFT ===== Pitch: Create NFTs representing unique narrative characters to securely license and track derivative use. Page: https://arcprompts.lovable.app/ideas/writing-characternft-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "CharacterNFT" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create NFTs representing unique narrative characters to securely license and track derivative use. Discipline: Writing, Poetry & Narrative (narrative IP). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts validate exclusive character ownership and transfer rights. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CharacterNFT.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title CharacterNFT /// @notice Create NFTs representing unique narrative characters to securely license and track derivative use. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract CharacterNFT { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CharacterNFT` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CharacterNFT.sol CharacterNFT` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a narrative IP action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-collaborative-verse-chain-14 · writing · Collaborative Verse Chain ===== Pitch: Immutable shared poetry projects pinned with full authorship and version control. Page: https://arcprompts.lovable.app/ideas/writing-collaborative-verse-chain-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Collaborative Verse Chain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Immutable shared poetry projects pinned with full authorship and version control. Discipline: Writing, Poetry & Narrative (co-written poetry). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS ensures no loss of collaborative poem versions via Pinata's reliable pinning service. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CollaborativeVerseChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentCollaborativeVerseChain /// @notice Immutable shared poetry projects pinned with full authorship and version control. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentCollaborativeVerseChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CollaborativeVerseChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CollaborativeVerseChain.sol CollaborativeVerseChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a co-written poetry, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-crowdwritten-saga-21 · writing · Crowdwritten Saga ===== Pitch: Pin evolving crowd-sourced story manifests for shared narrative ownership and history. Page: https://arcprompts.lovable.app/ideas/writing-crowdwritten-saga-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Crowdwritten Saga" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin evolving crowd-sourced story manifests for shared narrative ownership and history. Discipline: Writing, Poetry & Narrative (crowdsourced storytelling). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS with Pinata ensures every crowd addition is permanently stored with provenance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/CrowdwrittenSaga.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentCrowdwrittenSaga /// @notice Pin evolving crowd-sourced story manifests for shared narrative ownership and history. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentCrowdwrittenSaga { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs CrowdwrittenSaga` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/CrowdwrittenSaga.sol CrowdwrittenSaga` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a crowdsourced storytelling, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-dialogue-dao-21 · writing · Dialogue DAO ===== Pitch: Mint dialogue NFTs for decentralized collaboration with verified authorship. Page: https://arcprompts.lovable.app/ideas/writing-dialogue-dao-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dialogue DAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint dialogue NFTs for decentralized collaboration with verified authorship. Discipline: Writing, Poetry & Narrative (scriptwriting collaboration). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens provide transparent proof for multi-writer dialogue ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DialogueDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title DialogueDAO /// @notice ERC-721 provenance for: Mint dialogue NFTs for decentralized collaboration with verified authorship. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DialogueDAO is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("DialogueDAO", "DIALOG") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DialogueDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DialogueDAO.sol DialogueDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a scriptwriting collaboration, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-dialogue-drop-7 · writing · Dialogue Drop ===== Pitch: Mint dialogues as NFTs to certify creation and facilitate transparent rights management. Page: https://arcprompts.lovable.app/ideas/writing-dialogue-drop-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Dialogue Drop" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint dialogues as NFTs to certify creation and facilitate transparent rights management. Discipline: Writing, Poetry & Narrative (scriptwriting dialogue). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 provenance tokens link dialogues to creators immutably 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DialogueDrop.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title DialogueDrop /// @notice ERC-721 provenance for: Mint dialogues as NFTs to certify creation and facilitate transparent rights management. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DialogueDrop is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("DialogueDrop", "DIALOG") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DialogueDrop` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DialogueDrop.sol DialogueDrop` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a scriptwriting dialogue, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-dialoguedao-19 · writing · DialogueDAO ===== Pitch: Govern and reward co-writers contributing dialogue lines via blockchain DAO mechanisms. Page: https://arcprompts.lovable.app/ideas/writing-dialoguedao-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "DialogueDAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Govern and reward co-writers contributing dialogue lines via blockchain DAO mechanisms. Discipline: Writing, Poetry & Narrative (script collaboration). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts automatically record and share governance and revenue fairly among writers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/DialogueDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title DialogueDAO /// @notice Govern and reward co-writers contributing dialogue lines via blockchain DAO mechanisms. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract DialogueDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs DialogueDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/DialogueDAO.sol DialogueDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a script collaboration action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-echoedit-15 · writing · EchoEdit ===== Pitch: Exchange onchain peer critiques with gasless transactions tied to verified social identities. Page: https://arcprompts.lovable.app/ideas/writing-echoedit-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "EchoEdit" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Exchange onchain peer critiques with gasless transactions tied to verified social identities. Discipline: Writing, Poetry & Narrative (peer feedback). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored tx plus Privy wallet enables cost-free, traceable feedback exchanges. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EchoEdit.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogEchoEdit /// @notice Exchange onchain peer critiques with gasless transactions tied to verified social identities. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogEchoEdit { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EchoEdit` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EchoEdit.sol EchoEdit` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every peer feedback action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-echoessays-20 · writing · EchoEssays ===== Pitch: Publish and securely timestamp essays with gasless wallet transactions for provenance. Page: https://arcprompts.lovable.app/ideas/writing-echoessays-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "EchoEssays" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Publish and securely timestamp essays with gasless wallet transactions for provenance. Discipline: Writing, Poetry & Narrative (narrative essays). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy and sponsored tx ensure effortless essay proofing onchain without gas costs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EchoEssays.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogEchoEssays /// @notice Publish and securely timestamp essays with gasless wallet transactions for provenance. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogEchoEssays { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EchoEssays` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EchoEssays.sol EchoEssays` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every narrative essays action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-epic-editions-5 · writing · Epic Editions ===== Pitch: Mint chapters of novels as NFTs to prove authorship and track unpublished drafts. Page: https://arcprompts.lovable.app/ideas/writing-epic-editions-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Epic Editions" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint chapters of novels as NFTs to prove authorship and track unpublished drafts. Discipline: Writing, Poetry & Narrative (long-form fiction). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens anchor each chapter’s IPFS CID immutably 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EpicEditions.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title EpicEditions /// @notice ERC-721 provenance for: Mint chapters of novels as NFTs to prove authorship and track unpublished drafts. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract EpicEditions is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("EpicEditions", "EPICED") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EpicEditions` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EpicEditions.sol EpicEditions` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a long-form fiction, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-epic-endorse-18 · writing · Epic Endorse ===== Pitch: Mint peer review notes as NFTs to transparently acknowledge contributions to works. Page: https://arcprompts.lovable.app/ideas/writing-epic-endorse-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Epic Endorse" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint peer review notes as NFTs to transparently acknowledge contributions to works. Discipline: Writing, Poetry & Narrative (literary peer review). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance tracks reviewer identity and feedback immutably. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EpicEndorse.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title EpicEndorse /// @notice ERC-721 provenance for: Mint peer review notes as NFTs to transparently acknowledge contributions to works. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract EpicEndorse is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("EpicEndorse", "EPICEN") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EpicEndorse` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EpicEndorse.sol EpicEndorse` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a literary peer review, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-epic-poem-archive-12 · writing · Epic Poem Archive ===== Pitch: Host epic poems and iterations permanently ensuring literary longevity and accessibility. Page: https://arcprompts.lovable.app/ideas/writing-epic-poem-archive-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Epic Poem Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Host epic poems and iterations permanently ensuring literary longevity and accessibility. Discipline: Writing, Poetry & Narrative (long-form poetry). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS via Pinata provides decentralized, censorship-resistant storage for large poetic works. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/EpicPoemArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentEpicPoemArchive /// @notice Host epic poems and iterations permanently ensuring literary longevity and accessibility. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentEpicPoemArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs EpicPoemArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/EpicPoemArchive.sol EpicPoemArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a long-form poetry, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-fable-frame-4 · writing · Fable Frame ===== Pitch: Mint children’s story drafts as NFTs to track authorship and distribution rights. Page: https://arcprompts.lovable.app/ideas/writing-fable-frame-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Fable Frame" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint children’s story drafts as NFTs to track authorship and distribution rights. Discipline: Writing, Poetry & Narrative (children’s narratives). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance provides transparent proof of original content and creator. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FableFrame.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title FableFrame /// @notice ERC-721 provenance for: Mint children’s story drafts as NFTs to track authorship and distribution rights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FableFrame is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("FableFrame", "FABLEF") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FableFrame` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FableFrame.sol FableFrame` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a children’s narratives, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-fiction-flow-23 · writing · Fiction Flow ===== Pitch: Mint serialized fiction episodes as NFTs to prove originality and ownership over time. Page: https://arcprompts.lovable.app/ideas/writing-fiction-flow-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Fiction Flow" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint serialized fiction episodes as NFTs to prove originality and ownership over time. Discipline: Writing, Poetry & Narrative (serialized fiction). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens provide provenance for sequential episode content. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FictionFlow.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title FictionFlow /// @notice ERC-721 provenance for: Mint serialized fiction episodes as NFTs to prove originality and ownership over time. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FictionFlow is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("FictionFlow", "FICTIO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FictionFlow` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FictionFlow.sol FictionFlow` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a serialized fiction, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-fictionfuel-6 · writing · FictionFuel ===== Pitch: Collaborate on story ideas privately with gasless transactions to ensure smooth feedback flow. Page: https://arcprompts.lovable.app/ideas/writing-fictionfuel-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FictionFuel" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Collaborate on story ideas privately with gasless transactions to ensure smooth feedback flow. Discipline: Writing, Poetry & Narrative (story ideation). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social wallet enables private idea exchange without hassle of paying gas fees. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FictionFuel.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogFictionFuel /// @notice Collaborate on story ideas privately with gasless transactions to ensure smooth feedback flow. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogFictionFuel { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FictionFuel` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FictionFuel.sol FictionFuel` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every story ideation action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-flash-fiction-cache-13 · writing · Flash Fiction Cache ===== Pitch: Pin flash fiction and micro-narratives ensuring permanent public access and author credit. Page: https://arcprompts.lovable.app/ideas/writing-flash-fiction-cache-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Flash Fiction Cache" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin flash fiction and micro-narratives ensuring permanent public access and author credit. Discipline: Writing, Poetry & Narrative (short story publishing). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata allows fast, immutable pinning of small story JSONs and cover art on IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FlashFictionCache.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentFlashFictionCache /// @notice Pin flash fiction and micro-narratives ensuring permanent public access and author credit. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentFlashFictionCache { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FlashFictionCache` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FlashFictionCache.sol FlashFictionCache` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a short story publishing, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-freeverse-forge-6 · writing · Freeverse Forge ===== Pitch: Mint free verse poems as creator-owned NFTs with guaranteed provenance and originality. Page: https://arcprompts.lovable.app/ideas/writing-freeverse-forge-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Freeverse Forge" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint free verse poems as creator-owned NFTs with guaranteed provenance and originality. Discipline: Writing, Poetry & Narrative (experimental poetry). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs provide secure, decentralized proof of authorship for unique poetic works. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FreeverseForge.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title FreeverseForge /// @notice ERC-721 provenance for: Mint free verse poems as creator-owned NFTs with guaranteed provenance and originality. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FreeverseForge is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("FreeverseForge", "FREEVE") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FreeverseForge` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FreeverseForge.sol FreeverseForge` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a experimental poetry, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-freeversedao-12 · writing · FreeVerseDAO ===== Pitch: Enable decentralized funding and grant distribution for poets via DAO governance onchain. Page: https://arcprompts.lovable.app/ideas/writing-freeversedao-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "FreeVerseDAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable decentralized funding and grant distribution for poets via DAO governance onchain. Discipline: Writing, Poetry & Narrative (poetry funding). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts transparently allocate funds based on community votes and milestones. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/FreeVerseDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title FreeVerseDAO /// @notice Enable decentralized funding and grant distribution for poets via DAO governance onchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract FreeVerseDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs FreeVerseDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/FreeVerseDAO.sol FreeVerseDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a poetry funding action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-haiku-hub-11 · writing · Haiku Hub ===== Pitch: Mint haiku poems as unique NFTs to secure authorship with decentralized verification. Page: https://arcprompts.lovable.app/ideas/writing-haiku-hub-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Haiku Hub" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint haiku poems as unique NFTs to secure authorship with decentralized verification. Discipline: Writing, Poetry & Narrative (micro-poetry). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens link haikus to IPFS content ensuring creator provenance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/HaikuHub.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title HaikuHub /// @notice ERC-721 provenance for: Mint haiku poems as unique NFTs to secure authorship with decentralized verification. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract HaikuHub is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("HaikuHub", "HAIKUH") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs HaikuHub` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/HaikuHub.sol HaikuHub` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a micro-poetry, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-lyric-link-15 · writing · Lyric Link ===== Pitch: Mint song lyrics as NFTs to certify authorship and protect intellectual property. Page: https://arcprompts.lovable.app/ideas/writing-lyric-link-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Lyric Link" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint song lyrics as NFTs to certify authorship and protect intellectual property. Discipline: Writing, Poetry & Narrative (songwriting lyrics). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens anchor lyrics’ IPFS CIDs ensuring creator provenance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/LyricLink.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title LyricLink /// @notice ERC-721 provenance for: Mint song lyrics as NFTs to certify authorship and protect intellectual property. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract LyricLink is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("LyricLink", "LYRICL") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs LyricLink` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/LyricLink.sol LyricLink` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a songwriting lyrics, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-memoir-mint-14 · writing · Memoir Mint ===== Pitch: Mint memoir excerpts as NFTs to preserve authentic creator ownership and history. Page: https://arcprompts.lovable.app/ideas/writing-memoir-mint-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Memoir Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint memoir excerpts as NFTs to preserve authentic creator ownership and history. Discipline: Writing, Poetry & Narrative (personal narratives). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance on Sepolia guarantees immutable creator proof for personal tales. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MemoirMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MemoirMint /// @notice ERC-721 provenance for: Mint memoir excerpts as NFTs to preserve authentic creator ownership and history. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MemoirMint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MemoirMint", "MEMOIR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MemoirMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MemoirMint.sol MemoirMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a personal narratives, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-metadata-story-weave-11 · writing · Metadata Story Weave ===== Pitch: Attach and pin rich metadata sets to stories ensuring permanent discoverability and context. Page: https://arcprompts.lovable.app/ideas/writing-metadata-story-weave-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Metadata Story Weave" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Attach and pin rich metadata sets to stories ensuring permanent discoverability and context. Discipline: Writing, Poetry & Narrative (story metadata management). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT upload pins complex JSON metadata to IPFS for immutable story context. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MetadataStoryWeave.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentMetadataStoryWeave /// @notice Attach and pin rich metadata sets to stories ensuring permanent discoverability and context. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentMetadataStoryWeave { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MetadataStoryWeave` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MetadataStoryWeave.sol MetadataStoryWeave` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a story metadata management, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-metapoem-8 · writing · MetaPoem ===== Pitch: Create poems that evolve via blockchain triggers based on reader interactions or events. Page: https://arcprompts.lovable.app/ideas/writing-metapoem-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MetaPoem" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create poems that evolve via blockchain triggers based on reader interactions or events. Discipline: Writing, Poetry & Narrative (dynamic poetry). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enable programmable, immutable poem state changes. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MetaPoem.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title MetaPoem /// @notice Create poems that evolve via blockchain triggers based on reader interactions or events. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MetaPoem { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MetaPoem` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MetaPoem.sol MetaPoem` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a dynamic poetry action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-musemint-22 · writing · MuseMint ===== Pitch: Mint prompt NFTs to claim originality and track prompt provenance for writers. Page: https://arcprompts.lovable.app/ideas/writing-musemint-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "MuseMint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint prompt NFTs to claim originality and track prompt provenance for writers. Discipline: Writing, Poetry & Narrative (creative prompts). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFTs ensure immutable proof of prompt creation and ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MuseMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MuseMint /// @notice ERC-721 provenance for: Mint prompt NFTs to claim originality and track prompt provenance for writers. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MuseMint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MuseMint", "MUSEMI") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MuseMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MuseMint.sol MuseMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a creative prompts, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-mythos-mint-8 · writing · Mythos Mint ===== Pitch: Mint lore pieces as NFTs to secure original world-building content and creator identity. Page: https://arcprompts.lovable.app/ideas/writing-mythos-mint-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Mythos Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint lore pieces as NFTs to secure original world-building content and creator identity. Discipline: Writing, Poetry & Narrative (world-building lore). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance anchors creator-owned lore on-chain with IPFS verification. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/MythosMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title MythosMint /// @notice ERC-721 provenance for: Mint lore pieces as NFTs to secure original world-building content and creator identity. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract MythosMint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("MythosMint", "MYTHOS") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs MythosMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/MythosMint.sol MythosMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a world-building lore, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narratenestnft-22 · writing · NarrateNestNFT ===== Pitch: Mint and trade world-building NFTs with seamless, gasless wallet integration. Page: https://arcprompts.lovable.app/ideas/writing-narratenestnft-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NarrateNestNFT" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint and trade world-building NFTs with seamless, gasless wallet integration. Discipline: Writing, Poetry & Narrative (world-building NFTs). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored transactions minimize minting and transfer costs for expansive narrative NFTs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrateNestNFT.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogNarrateNestNFT /// @notice Mint and trade world-building NFTs with seamless, gasless wallet integration. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogNarrateNestNFT { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrateNestNFT` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrateNestNFT.sol NarrateNestNFT` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every world-building NFTs action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narratenexus-3 · writing · NarrateNexus ===== Pitch: Create and monetize narrative assets securely with seamless wallet integration and no fees. Page: https://arcprompts.lovable.app/ideas/writing-narratenexus-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NarrateNexus" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create and monetize narrative assets securely with seamless wallet integration and no fees. Discipline: Writing, Poetry & Narrative (narrative design). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet enables easy payments and asset transfers without user gas burden. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrateNexus.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogNarrateNexus /// @notice Create and monetize narrative assets securely with seamless wallet integration and no fees. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogNarrateNexus { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrateNexus` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrateNexus.sol NarrateNexus` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every narrative design action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narratenft-11 · writing · NarrateNFT ===== Pitch: Mint story elements as NFTs with embedded, gasless wallet integration for easy ownership. Page: https://arcprompts.lovable.app/ideas/writing-narratenft-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NarrateNFT" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint story elements as NFTs with embedded, gasless wallet integration for easy ownership. Discipline: Writing, Poetry & Narrative (narrative NFTs). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored transactions minimize cost barriers in minting and transferring narrative NFTs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrateNFT.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogNarrateNFT /// @notice Mint story elements as NFTs with embedded, gasless wallet integration for easy ownership. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogNarrateNFT { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrateNFT` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrateNFT.sol NarrateNFT` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every narrative NFTs action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narrative-artifact-ledger-15 · writing · Narrative Artifact Ledger ===== Pitch: Preserve story artifacts, lore entries and related media immutably linked to narratives. Page: https://arcprompts.lovable.app/ideas/writing-narrative-artifact-ledger-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Narrative Artifact Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Preserve story artifacts, lore entries and related media immutably linked to narratives. Discipline: Writing, Poetry & Narrative (story props and lore). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata uploads JSON/image assets on IPFS providing permanent lore provenance. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrativeArtifactLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentNarrativeArtifactLedger /// @notice Preserve story artifacts, lore entries and related media immutably linked to narratives. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentNarrativeArtifactLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrativeArtifactLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrativeArtifactLedger.sol NarrativeArtifactLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a story props and lore, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narrative-device-index-20 · writing · Narrative Device Index ===== Pitch: Create permanent indexes of literary devices used in stories with examples and annotations. Page: https://arcprompts.lovable.app/ideas/writing-narrative-device-index-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Narrative Device Index" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create permanent indexes of literary devices used in stories with examples and annotations. Discipline: Writing, Poetry & Narrative (literary device cataloging). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s IPFS pinning preserves searchable JSON device catalogs and linked media. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrativeDeviceIndex.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentNarrativeDeviceIndex /// @notice Create permanent indexes of literary devices used in stories with examples and annotations. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentNarrativeDeviceIndex { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrativeDeviceIndex` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrativeDeviceIndex.sol NarrativeDeviceIndex` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a literary device cataloging, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narrative-flow-sync-1 · writing · Narrative Flow Sync ===== Pitch: Save and share branching story manifests guaranteeing permanent access and collaboration. Page: https://arcprompts.lovable.app/ideas/writing-narrative-flow-sync-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Narrative Flow Sync" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Save and share branching story manifests guaranteeing permanent access and collaboration. Discipline: Writing, Poetry & Narrative (interactive storytelling). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS via Pinata securely pins dynamic narrative manifests ensuring consistent version control. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrativeFlowSync.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentNarrativeFlowSync /// @notice Save and share branching story manifests guaranteeing permanent access and collaboration. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentNarrativeFlowSync { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrativeFlowSync` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrativeFlowSync.sol NarrativeFlowSync` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a interactive storytelling, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narrative-map-archive-5 · writing · Narrative Map Archive ===== Pitch: Securely store interactive narrative maps and their changes for narrative design teams. Page: https://arcprompts.lovable.app/ideas/writing-narrative-map-archive-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Narrative Map Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely store interactive narrative maps and their changes for narrative design teams. Discipline: Writing, Poetry & Narrative (story mapping). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinning JSON manifests on IPFS provides decentralization and permanence for story maps. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrativeMapArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentNarrativeMapArchive /// @notice Securely store interactive narrative maps and their changes for narrative design teams. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentNarrativeMapArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrativeMapArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrativeMapArchive.sol NarrativeMapArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a story mapping, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narrative-nexus-1 · writing · Narrative Nexus ===== Pitch: Mint branching story nodes as NFTs to ensure unique creator control and provenance. Page: https://arcprompts.lovable.app/ideas/writing-narrative-nexus-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Narrative Nexus" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint branching story nodes as NFTs to ensure unique creator control and provenance. Discipline: Writing, Poetry & Narrative (interactive storytelling). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens anchor each story branch immutably on-chain with IPFS content. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrativeNexus.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title NarrativeNexus /// @notice ERC-721 provenance for: Mint branching story nodes as NFTs to ensure unique creator control and provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract NarrativeNexus is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("NarrativeNexus", "NARRAT") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrativeNexus` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrativeNexus.sol NarrativeNexus` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a interactive storytelling, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narrative-nexus-19 · writing · Narrative Nexus ===== Pitch: Mint branching game story elements as NFTs to track creators and version history. Page: https://arcprompts.lovable.app/ideas/writing-narrative-nexus-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Narrative Nexus" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint branching game story elements as NFTs to track creators and version history. Discipline: Writing, Poetry & Narrative (game narrative design). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens anchor unique narrative nodes securely 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrativeNexus.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title NarrativeNexus /// @notice ERC-721 provenance for: Mint branching game story elements as NFTs to track creators and version history. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract NarrativeNexus is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("NarrativeNexus", "NARRAT") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrativeNexus` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrativeNexus.sol NarrativeNexus` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a game narrative design, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narrative-role-tracker-18 · writing · Narrative Role Tracker ===== Pitch: Immutable tracking of story roles and character arcs pinned for collaborative clarity. Page: https://arcprompts.lovable.app/ideas/writing-narrative-role-tracker-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Narrative Role Tracker" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Immutable tracking of story roles and character arcs pinned for collaborative clarity. Discipline: Writing, Poetry & Narrative (role-based story design). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS via Pinata pins JSON role manifests securely ensuring consistent team access. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrativeRoleTracker.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentNarrativeRoleTracker /// @notice Immutable tracking of story roles and character arcs pinned for collaborative clarity. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentNarrativeRoleTracker { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrativeRoleTracker` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrativeRoleTracker.sol NarrativeRoleTracker` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a role-based story design, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narrative-soundtrack-archive-10 · writing · Narrative Soundtrack Archive ===== Pitch: Store narrative audio clips and story-related soundtracks permanently linked to story manifests. Page: https://arcprompts.lovable.app/ideas/writing-narrative-soundtrack-archive-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Narrative Soundtrack Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Store narrative audio clips and story-related soundtracks permanently linked to story manifests. Discipline: Writing, Poetry & Narrative (audio storytelling). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata supports pinning audio files with metadata immutably on IPFS for storytelling. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrativeSoundtrackArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentNarrativeSoundtrackArchive /// @notice Store narrative audio clips and story-related soundtracks permanently linked to story manifests. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentNarrativeSoundtrackArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrativeSoundtrackArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrativeSoundtrackArchive.sol NarrativeSoundtrackArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a audio storytelling, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narrative-timeline-archive-24 · writing · Narrative Timeline Archive ===== Pitch: Pin chronological story timelines with events and character arcs permanently stored. Page: https://arcprompts.lovable.app/ideas/writing-narrative-timeline-archive-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Narrative Timeline Archive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin chronological story timelines with events and character arcs permanently stored. Discipline: Writing, Poetry & Narrative (chronology management). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata securely pins timeline JSONs to IPFS preserving story chronology over time. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrativeTimelineArchive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentNarrativeTimelineArchive /// @notice Pin chronological story timelines with events and character arcs permanently stored. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentNarrativeTimelineArchive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrativeTimelineArchive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrativeTimelineArchive.sol NarrativeTimelineArchive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a chronology management, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narrative-token-ledger-7 · writing · Narrative Token Ledger ===== Pitch: Register and prove narrative intellectual property rights with permanent onchain manifests. Page: https://arcprompts.lovable.app/ideas/writing-narrative-token-ledger-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Narrative Token Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Register and prove narrative intellectual property rights with permanent onchain manifests. Discipline: Writing, Poetry & Narrative (story ownership). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Immutable IPFS pins via Pinata provide verifiable proof of story originality and timestamps. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrativeTokenLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentNarrativeTokenLedger /// @notice Register and prove narrative intellectual property rights with permanent onchain manifests. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentNarrativeTokenLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrativeTokenLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrativeTokenLedger.sol NarrativeTokenLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a story ownership, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narrativebadge-13 · writing · NarrativeBadge ===== Pitch: Issue verifiable badges or certificates on Sepolia to recognize narrative design skills and achievements. Page: https://arcprompts.lovable.app/ideas/writing-narrativebadge-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NarrativeBadge" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Issue verifiable badges or certificates on Sepolia to recognize narrative design skills and achievements. Discipline: Writing, Poetry & Narrative (writer credentials). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts securely mint badges that cannot be falsified or revoked unfairly. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrativeBadge.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title NarrativeBadge /// @notice Issue verifiable badges or certificates on Sepolia to recognize narrative design skills and achievements. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract NarrativeBadge { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrativeBadge` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrativeBadge.sol NarrativeBadge` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a writer credentials action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narrativemint-22 · writing · NarrativeMint ===== Pitch: Mint entire stories as tokens allowing ownership proof and fractional sales. Page: https://arcprompts.lovable.app/ideas/writing-narrativemint-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NarrativeMint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint entire stories as tokens allowing ownership proof and fractional sales. Discipline: Writing, Poetry & Narrative (story tokenization). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts enable immutable story token minting and ownership tracking. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrativeMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title NarrativeMint /// @notice Mint entire stories as tokens allowing ownership proof and fractional sales. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract NarrativeMint { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrativeMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrativeMint.sol NarrativeMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a story tokenization action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narrativenest-8 · writing · NarrativeNest ===== Pitch: Build and own expansive world lore collaboratively with gasless onchain edits and storage. Page: https://arcprompts.lovable.app/ideas/writing-narrativenest-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NarrativeNest" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Build and own expansive world lore collaboratively with gasless onchain edits and storage. Discipline: Writing, Poetry & Narrative (world-building). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet plus sponsored tx allows collaborative lore creation without blockchain fees. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrativeNest.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogNarrativeNest /// @notice Build and own expansive world lore collaboratively with gasless onchain edits and storage. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogNarrativeNest { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrativeNest` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrativeNest.sol NarrativeNest` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every world-building action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narrativenodes-14 · writing · NarrativeNodes ===== Pitch: Design interconnected story graphs collaboratively with frictionless onchain edits and ownership. Page: https://arcprompts.lovable.app/ideas/writing-narrativenodes-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NarrativeNodes" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Design interconnected story graphs collaboratively with frictionless onchain edits and ownership. Discipline: Writing, Poetry & Narrative (story graph design). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallets plus sponsored tx enable seamless multi-user graph changes without gas. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrativeNodes.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogNarrativeNodes /// @notice Design interconnected story graphs collaboratively with frictionless onchain edits and ownership. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogNarrativeNodes { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrativeNodes` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrativeNodes.sol NarrativeNodes` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every story graph design action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narrativestake-15 · writing · NarrativeStake ===== Pitch: Reward story contributors fairly by staking tokens linked to their contributions recorded onchain. Page: https://arcprompts.lovable.app/ideas/writing-narrativestake-15 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NarrativeStake" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Reward story contributors fairly by staking tokens linked to their contributions recorded onchain. Discipline: Writing, Poetry & Narrative (story contributor rewards). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts hold and distribute rewards based on transparent contribution metrics. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrativeStake.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title NarrativeStake /// @notice Reward story contributors fairly by staking tokens linked to their contributions recorded onchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract NarrativeStake { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrativeStake` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrativeStake.sol NarrativeStake` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a story contributor rewards action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narrativevote-1 · writing · NarrativeVote ===== Pitch: Enable readers to vote on story plot directions through blockchain-based decision contracts. Page: https://arcprompts.lovable.app/ideas/writing-narrativevote-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NarrativeVote" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable readers to vote on story plot directions through blockchain-based decision contracts. Discipline: Writing, Poetry & Narrative (interactive storytelling). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts transparently tally reader choices for decentralized story progression. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarrativeVote.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title NarrativeVote /// @notice Enable readers to vote on story plot directions through blockchain-based decision contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract NarrativeVote { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarrativeVote` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarrativeVote.sol NarrativeVote` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a interactive storytelling action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narrator-s-nod-16 · writing · Narrator’s Nod ===== Pitch: Mint audiobook scripts as NFTs to prove original authorship and distribution rights. Page: https://arcprompts.lovable.app/ideas/writing-narrator-s-nod-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Narrator’s Nod" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint audiobook scripts as NFTs to prove original authorship and distribution rights. Discipline: Writing, Poetry & Narrative (audiobook scripts). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance secures scripts immutably on-chain with IPFS storage. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarratorsNod.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title NarratorsNod /// @notice ERC-721 provenance for: Mint audiobook scripts as NFTs to prove original authorship and distribution rights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract NarratorsNod is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("NarratorsNod", "NARRAT") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarratorsNod` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarratorsNod.sol NarratorsNod` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a audiobook scripts, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-narratordao-6 · writing · NarratorDAO ===== Pitch: Create decentralized stories collectively owned and governed by contributors via DAO smart contracts. Page: https://arcprompts.lovable.app/ideas/writing-narratordao-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "NarratorDAO" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create decentralized stories collectively owned and governed by contributors via DAO smart contracts. Discipline: Writing, Poetry & Narrative (community storytelling). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts enforce democratic participation and profit sharing in story creation. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/NarratorDAO.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title NarratorDAO /// @notice Create decentralized stories collectively owned and governed by contributors via DAO smart contracts. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract NarratorDAO { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs NarratorDAO` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/NarratorDAO.sol NarratorDAO` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a community storytelling action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-plot-mint-17 · writing · Plot Mint ===== Pitch: Mint story concepts as NFTs to validate originality and stake creative claims. Page: https://arcprompts.lovable.app/ideas/writing-plot-mint-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Plot Mint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint story concepts as NFTs to validate originality and stake creative claims. Discipline: Writing, Poetry & Narrative (story idea validation). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens provide early immutable proof of concept and ownership. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlotMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PlotMint /// @notice ERC-721 provenance for: Mint story concepts as NFTs to validate originality and stake creative claims. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PlotMint is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PlotMint", "PLOTMI") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlotMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlotMint.sol PlotMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a story idea validation, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-plotchainswap-21 · writing · PlotChainSwap ===== Pitch: Enable decentralized swapping and barter of plot ideas secured and logged on blockchain. Page: https://arcprompts.lovable.app/ideas/writing-plotchainswap-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PlotChainSwap" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable decentralized swapping and barter of plot ideas secured and logged on blockchain. Discipline: Writing, Poetry & Narrative (story idea exchange). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts facilitate trustless, transparent exchanges without intermediaries. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlotChainSwap.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PlotChainSwap /// @notice Enable decentralized swapping and barter of plot ideas secured and logged on blockchain. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PlotChainSwap { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlotChainSwap` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlotChainSwap.sol PlotChainSwap` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a story idea exchange action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-plotpassport-16 · writing · PlotPassport ===== Pitch: Securely register story beginnings onchain with gasless wallet transactions for easy proof. Page: https://arcprompts.lovable.app/ideas/writing-plotpassport-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PlotPassport" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely register story beginnings onchain with gasless wallet transactions for easy proof. Discipline: Writing, Poetry & Narrative (story ownership). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social and sponsored tx allow authors to claim story rights cost-free and instantly. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlotPassport.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPlotPassport /// @notice Securely register story beginnings onchain with gasless wallet transactions for easy proof. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPlotPassport { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlotPassport` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlotPassport.sol PlotPassport` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every story ownership action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-plotpledge-10 · writing · PlotPledge ===== Pitch: Pledge funds to promising stories with seamless, gasless blockchain sponsorship wallets. Page: https://arcprompts.lovable.app/ideas/writing-plotpledge-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PlotPledge" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pledge funds to promising stories with seamless, gasless blockchain sponsorship wallets. Discipline: Writing, Poetry & Narrative (crowdfund writing). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet with sponsored tx streamlines writer crowdfunding without gas hurdles. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlotPledge.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPlotPledge /// @notice Pledge funds to promising stories with seamless, gasless blockchain sponsorship wallets. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPlotPledge { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlotPledge` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlotPledge.sol PlotPledge` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every crowdfund writing action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-plotpoint-proof-10 · writing · PlotPoint Proof ===== Pitch: Mint plot outlines as NFTs to prove original story structures and creator authorship. Page: https://arcprompts.lovable.app/ideas/writing-plotpoint-proof-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PlotPoint Proof" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint plot outlines as NFTs to prove original story structures and creator authorship. Discipline: Writing, Poetry & Narrative (story plotting). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance ensures immutable proof of plot ownership and content. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlotPointProof.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PlotPointProof /// @notice ERC-721 provenance for: Mint plot outlines as NFTs to prove original story structures and creator authorship. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PlotPointProof is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PlotPointProof", "PLOTPO") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlotPointProof` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlotPointProof.sol PlotPointProof` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a story plotting, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-plotproof-4 · writing · PlotProof ===== Pitch: Timestamp story drafts to prove originality and track narrative evolution over time. Page: https://arcprompts.lovable.app/ideas/writing-plotproof-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PlotProof" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Timestamp story drafts to prove originality and track narrative evolution over time. Discipline: Writing, Poetry & Narrative (story validation). Onchain primitive: Arc smart contract. Why this primitive: Immutable timestamps on Sepolia verify manuscript creation moments reliably and publicly. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlotProof.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PlotProof /// @notice Timestamp story drafts to prove originality and track narrative evolution over time. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PlotProof { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlotProof` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlotProof.sol PlotProof` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a story validation action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-plotpulse-23 · writing · PlotPulse ===== Pitch: Analyze reader engagement on stories with gasless onchain interaction tracking. Page: https://arcprompts.lovable.app/ideas/writing-plotpulse-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PlotPulse" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Analyze reader engagement on stories with gasless onchain interaction tracking. Discipline: Writing, Poetry & Narrative (story analytics). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social wallet and sponsored tx enable cost-free interaction analytics onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlotPulse.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPlotPulse /// @notice Analyze reader engagement on stories with gasless onchain interaction tracking. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPlotPulse { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlotPulse` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlotPulse.sol PlotPulse` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every story analytics action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-plottoken-10 · writing · PlotToken ===== Pitch: Tokenize plot ideas for secure licensing and transfer to producers or collaborators. Page: https://arcprompts.lovable.app/ideas/writing-plottoken-10 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PlotToken" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Tokenize plot ideas for secure licensing and transfer to producers or collaborators. Discipline: Writing, Poetry & Narrative (story licensing). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts guarantee ownership and limit unauthorized reuse through token control. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PlotToken.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PlotToken /// @notice Tokenize plot ideas for secure licensing and transfer to producers or collaborators. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PlotToken { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PlotToken` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PlotToken.sol PlotToken` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a story licensing action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-poem-remix-repository-8 · writing · Poem Remix Repository ===== Pitch: Host and pin collaborative poem remixes with full version history on decentralized storage. Page: https://arcprompts.lovable.app/ideas/writing-poem-remix-repository-8 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Poem Remix Repository" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Host and pin collaborative poem remixes with full version history on decentralized storage. Discipline: Writing, Poetry & Narrative (poetry collaboration). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata guarantees all remixed poem versions remain permanently accessible on IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PoemRemixRepository.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPoemRemixRepository /// @notice Host and pin collaborative poem remixes with full version history on decentralized storage. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPoemRemixRepository { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PoemRemixRepository` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PoemRemixRepository.sol PoemRemixRepository` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a poetry collaboration, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-poemchainprint-14 · writing · PoemChainPrint ===== Pitch: Create blockchain-verified limited edition poem prints with provable scarcity for collectors. Page: https://arcprompts.lovable.app/ideas/writing-poemchainprint-14 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PoemChainPrint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create blockchain-verified limited edition poem prints with provable scarcity for collectors. Discipline: Writing, Poetry & Narrative (limited edition poetry). Onchain primitive: Arc smart contract. Why this primitive: Immutable smart contracts verify scarcity and provenance of each print edition. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PoemChainPrint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PoemChainPrint /// @notice Create blockchain-verified limited edition poem prints with provable scarcity for collectors. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PoemChainPrint { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PoemChainPrint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PoemChainPrint.sol PoemChainPrint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a limited edition poetry action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-poemproof-4 · writing · PoemProof ===== Pitch: Instantly timestamp poems with zero gas to prove authorship and originality. Page: https://arcprompts.lovable.app/ideas/writing-poemproof-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PoemProof" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Instantly timestamp poems with zero gas to prove authorship and originality. Discipline: Writing, Poetry & Narrative (poetry copyright). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored transactions allow effortless onchain proof of poem creation without cost barriers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PoemProof.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogPoemProof /// @notice Instantly timestamp poems with zero gas to prove authorship and originality. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogPoemProof { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PoemProof` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PoemProof.sol PoemProof` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every poetry copyright action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-poet-s-provenance-3 · writing · Poet’s Provenance ===== Pitch: Create unique NFT tokens for lyric poems, certifying creators and preserving provenance. Page: https://arcprompts.lovable.app/ideas/writing-poet-s-provenance-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Poet’s Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Create unique NFT tokens for lyric poems, certifying creators and preserving provenance. Discipline: Writing, Poetry & Narrative (lyric poetry). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 on Sepolia secures immutable poet ownership metadata. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PoetsProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PoetsProvenance /// @notice ERC-721 provenance for: Create unique NFT tokens for lyric poems, certifying creators and preserving provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PoetsProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PoetsProvenance", "POETSP") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PoetsProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PoetsProvenance.sol PoetsProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a lyric poetry, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-poetchain-3 · writing · PoetChain ===== Pitch: Facilitate co-authored poems with edit histories securely stored on blockchain for trust. Page: https://arcprompts.lovable.app/ideas/writing-poetchain-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PoetChain" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Facilitate co-authored poems with edit histories securely stored on blockchain for trust. Discipline: Writing, Poetry & Narrative (collaborative poetry). Onchain primitive: Arc smart contract. Why this primitive: Sepolia smart contracts maintain immutable collaboration logs to verify contributions. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PoetChain.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PoetChain /// @notice Facilitate co-authored poems with edit histories securely stored on blockchain for trust. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PoetChain { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PoetChain` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PoetChain.sol PoetChain` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a collaborative poetry action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-poetic-memory-vault-0 · writing · Poetic Memory Vault ===== Pitch: Permanently preserve poets' drafts and revisions for authentic literary heritage. Page: https://arcprompts.lovable.app/ideas/writing-poetic-memory-vault-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Poetic Memory Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Permanently preserve poets' drafts and revisions for authentic literary heritage. Discipline: Writing, Poetry & Narrative (poetry archiving). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS ensures immutable, decentralized storage of evolving poetic content via Pinata. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PoeticMemoryVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPoeticMemoryVault /// @notice Permanently preserve poets' drafts and revisions for authentic literary heritage. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPoeticMemoryVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PoeticMemoryVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PoeticMemoryVault.sol PoeticMemoryVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a poetry archiving, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-poetprints-20 · writing · PoetPrints ===== Pitch: Mint poetic works as NFTs to facilitate transparent licensing and royalty tracking. Page: https://arcprompts.lovable.app/ideas/writing-poetprints-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PoetPrints" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint poetic works as NFTs to facilitate transparent licensing and royalty tracking. Discipline: Writing, Poetry & Narrative (poetry licensing). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance ensures clear ownership for automated royalty flows. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PoetPrints.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title PoetPrints /// @notice ERC-721 provenance for: Mint poetic works as NFTs to facilitate transparent licensing and royalty tracking. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PoetPrints is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("PoetPrints", "POETPR") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PoetPrints` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PoetPrints.sol PoetPrints` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a poetry licensing, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-poetproofs-20 · writing · PoetProofs ===== Pitch: Notarize poem drafts with blockchain timestamps to establish authorship and protect IP rights. Page: https://arcprompts.lovable.app/ideas/writing-poetproofs-20 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PoetProofs" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Notarize poem drafts with blockchain timestamps to establish authorship and protect IP rights. Discipline: Writing, Poetry & Narrative (draft notarization). Onchain primitive: Arc smart contract. Why this primitive: Immutable onchain records provide trusted proof of creation time and authenticity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PoetProofs.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PoetProofs /// @notice Notarize poem drafts with blockchain timestamps to establish authorship and protect IP rights. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PoetProofs { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PoetProofs` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PoetProofs.sol PoetProofs` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a draft notarization action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-poetry-event-ledger-19 · writing · Poetry Event Ledger ===== Pitch: Pin event schedules, recordings and poems from live readings for lasting access. Page: https://arcprompts.lovable.app/ideas/writing-poetry-event-ledger-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Poetry Event Ledger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin event schedules, recordings and poems from live readings for lasting access. Discipline: Writing, Poetry & Narrative (live poetry events). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata pins multimedia event data on IPFS to preserve cultural moments immutably. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PoetryEventLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentPoetryEventLedger /// @notice Pin event schedules, recordings and poems from live readings for lasting access. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentPoetryEventLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PoetryEventLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PoetryEventLedger.sol PoetryEventLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a live poetry events, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-poetrychaingift-24 · writing · PoetryChainGift ===== Pitch: Gift poems securely onchain with immutable ownership and personalized provenance records. Page: https://arcprompts.lovable.app/ideas/writing-poetrychaingift-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PoetryChainGift" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Gift poems securely onchain with immutable ownership and personalized provenance records. Discipline: Writing, Poetry & Narrative (secure gifting). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts guarantee provenance and transfer history in gifting scenarios. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PoetryChainGift.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PoetryChainGift /// @notice Gift poems securely onchain with immutable ownership and personalized provenance records. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PoetryChainGift { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PoetryChainGift` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PoetryChainGift.sol PoetryChainGift` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a secure gifting action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-poetryescrow-17 · writing · PoetryEscrow ===== Pitch: Hold commissioned poem payments in escrow smart contracts until writer delivers and buyer approves. Page: https://arcprompts.lovable.app/ideas/writing-poetryescrow-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "PoetryEscrow" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Hold commissioned poem payments in escrow smart contracts until writer delivers and buyer approves. Discipline: Writing, Poetry & Narrative (commissioned writing). Onchain primitive: Arc smart contract. Why this primitive: Onchain escrow ensures trust and fairness in creative transactions. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/PoetryEscrow.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PoetryEscrow /// @notice Hold commissioned poem payments in escrow smart contracts until writer delivers and buyer approves. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PoetryEscrow { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs PoetryEscrow` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/PoetryEscrow.sol PoetryEscrow` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a commissioned writing action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-prose-provenance-13 · writing · Prose Provenance ===== Pitch: Mint short stories as NFTs to validate originality and protect author rights. Page: https://arcprompts.lovable.app/ideas/writing-prose-provenance-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Prose Provenance" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint short stories as NFTs to validate originality and protect author rights. Discipline: Writing, Poetry & Narrative (short story writing). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens provide unforgeable proof of story ownership and content. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ProseProvenance.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ProseProvenance /// @notice ERC-721 provenance for: Mint short stories as NFTs to validate originality and protect author rights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ProseProvenance is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ProseProvenance", "PROSEP") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ProseProvenance` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ProseProvenance.sol ProseProvenance` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a short story writing, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-scenemint-7 · writing · SceneMint ===== Pitch: Mint individual screenplay scenes as NFTs to license or sell specific script parts securely. Page: https://arcprompts.lovable.app/ideas/writing-scenemint-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "SceneMint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint individual screenplay scenes as NFTs to license or sell specific script parts securely. Discipline: Writing, Poetry & Narrative (script segments). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts verify uniqueness and ownership of discrete narrative elements. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SceneMint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SceneMint /// @notice Mint individual screenplay scenes as NFTs to license or sell specific script parts securely. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SceneMint { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SceneMint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SceneMint.sol SceneMint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a script segments action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-screenplay-beatboard-9 · writing · Screenplay Beatboard ===== Pitch: Pin structured screenplay beatboards and revisions immutably to maintain story consistency. Page: https://arcprompts.lovable.app/ideas/writing-screenplay-beatboard-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Screenplay Beatboard" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin structured screenplay beatboards and revisions immutably to maintain story consistency. Discipline: Writing, Poetry & Narrative (plot structuring). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS via Pinata securely stores serialized plot breakdowns with version control. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScreenplayBeatboard.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentScreenplayBeatboard /// @notice Pin structured screenplay beatboards and revisions immutably to maintain story consistency. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentScreenplayBeatboard { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScreenplayBeatboard` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScreenplayBeatboard.sol ScreenplayBeatboard` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a plot structuring, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-screenplay-casting-files-17 · writing · Screenplay Casting Files ===== Pitch: Pin casting notes and character references permanently linked to screenplay projects. Page: https://arcprompts.lovable.app/ideas/writing-screenplay-casting-files-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Screenplay Casting Files" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin casting notes and character references permanently linked to screenplay projects. Discipline: Writing, Poetry & Narrative (casting and character assets). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata JWT uploads preserve multi-format casting data immutably on IPFS. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScreenplayCastingFiles.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentScreenplayCastingFiles /// @notice Pin casting notes and character references permanently linked to screenplay projects. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentScreenplayCastingFiles { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScreenplayCastingFiles` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScreenplayCastingFiles.sol ScreenplayCastingFiles` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a casting and character assets, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-screenplay-snapshot-4 · writing · Screenplay Snapshot ===== Pitch: Pin your screenplay drafts with visual and metadata snapshots for secure historic reference. Page: https://arcprompts.lovable.app/ideas/writing-screenplay-snapshot-4 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Screenplay Snapshot" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin your screenplay drafts with visual and metadata snapshots for secure historic reference. Discipline: Writing, Poetry & Narrative (screenwriting versioning). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS via Pinata ensures permanent and tamper-proof screenplay version pinning. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScreenplaySnapshot.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentScreenplaySnapshot /// @notice Pin your screenplay drafts with visual and metadata snapshots for secure historic reference. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentScreenplaySnapshot { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScreenplaySnapshot` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScreenplaySnapshot.sol ScreenplaySnapshot` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a screenwriting versioning, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-screenplaystake-23 · writing · ScreenplayStake ===== Pitch: Community members stake tokens to review scripts securely, incentivizing quality feedback. Page: https://arcprompts.lovable.app/ideas/writing-screenplaystake-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScreenplayStake" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Community members stake tokens to review scripts securely, incentivizing quality feedback. Discipline: Writing, Poetry & Narrative (crowd script reviewing). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts manage stakes, rewards, and dispute resolution transparently. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScreenplayStake.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ScreenplayStake /// @notice Community members stake tokens to review scripts securely, incentivizing quality feedback. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ScreenplayStake { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScreenplayStake` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScreenplayStake.sol ScreenplayStake` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a crowd script reviewing action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-script-location-scouting-22 · writing · Script Location Scouting ===== Pitch: Pin images and notes of real locations linked to screenplay scenes immutably. Page: https://arcprompts.lovable.app/ideas/writing-script-location-scouting-22 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Script Location Scouting" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin images and notes of real locations linked to screenplay scenes immutably. Discipline: Writing, Poetry & Narrative (visual storytelling assets). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata uploads combine images and JSON on IPFS securing location scouting data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptLocationScouting.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentScriptLocationScouting /// @notice Pin images and notes of real locations linked to screenplay scenes immutably. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentScriptLocationScouting { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptLocationScouting` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptLocationScouting.sol ScriptLocationScouting` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a visual storytelling assets, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-script-stamp-2 · writing · Script Stamp ===== Pitch: Mint screenplay drafts as NFTs to certify original authorship and version history. Page: https://arcprompts.lovable.app/ideas/writing-script-stamp-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Script Stamp" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint screenplay drafts as NFTs to certify original authorship and version history. Discipline: Writing, Poetry & Narrative (screenwriting rights). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT minting records screenplay IPFS CIDs providing transparent ownership trails. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptStamp.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ScriptStamp /// @notice ERC-721 provenance for: Mint screenplay drafts as NFTs to certify original authorship and version history. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ScriptStamp is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ScriptStamp", "SCRIPT") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptStamp` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptStamp.sol ScriptStamp` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a screenwriting rights, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-scriptchainaudit-11 · writing · ScriptChainAudit ===== Pitch: Track and audit screenplay versions with immutable blockchain timestamps for dispute resolution. Page: https://arcprompts.lovable.app/ideas/writing-scriptchainaudit-11 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptChainAudit" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Track and audit screenplay versions with immutable blockchain timestamps for dispute resolution. Discipline: Writing, Poetry & Narrative (version tracking). Onchain primitive: Arc smart contract. Why this primitive: Sepolia contracts provide irrefutable, transparent version histories. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptChainAudit.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ScriptChainAudit /// @notice Track and audit screenplay versions with immutable blockchain timestamps for dispute resolution. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ScriptChainAudit { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptChainAudit` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptChainAudit.sol ScriptChainAudit` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a version tracking action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-scriptroyalty-2 · writing · ScriptRoyalty ===== Pitch: Automate royalty distribution for collaborative screenplays using verified smart contract agreements. Page: https://arcprompts.lovable.app/ideas/writing-scriptroyalty-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptRoyalty" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Automate royalty distribution for collaborative screenplays using verified smart contract agreements. Discipline: Writing, Poetry & Narrative (screenwriting rights). Onchain primitive: Arc smart contract. Why this primitive: Onchain contracts enforce transparent and automatic profit sharing among co-writers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptRoyalty.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ScriptRoyalty /// @notice Automate royalty distribution for collaborative screenplays using verified smart contract agreements. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ScriptRoyalty { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptRoyalty` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptRoyalty.sol ScriptRoyalty` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a screenwriting rights action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-scriptsphere-24 · writing · ScriptSphere ===== Pitch: Share scripts for peer review with gasless, onchain social wallet transactions. Page: https://arcprompts.lovable.app/ideas/writing-scriptsphere-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptSphere" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Share scripts for peer review with gasless, onchain social wallet transactions. Discipline: Writing, Poetry & Narrative (script feedback). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored tx with Privy wallet ensures seamless, costless script feedback exchange. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptSphere.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogScriptSphere /// @notice Share scripts for peer review with gasless, onchain social wallet transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogScriptSphere { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptSphere` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptSphere.sol ScriptSphere` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every script feedback action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-scriptsponsor-2 · writing · ScriptSponsor ===== Pitch: Collaborate on scripts with transparent, gasless edits tracked via embedded wallets. Page: https://arcprompts.lovable.app/ideas/writing-scriptsponsor-2 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptSponsor" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Collaborate on scripts with transparent, gasless edits tracked via embedded wallets. Discipline: Writing, Poetry & Narrative (screenwriting collaboration). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored tx ensure no gas fees interrupt smooth multi-author script edits. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptSponsor.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogScriptSponsor /// @notice Collaborate on scripts with transparent, gasless edits tracked via embedded wallets. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogScriptSponsor { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptSponsor` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptSponsor.sol ScriptSponsor` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every screenwriting collaboration action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-scriptsponsorswap-12 · writing · ScriptSponsorSwap ===== Pitch: Enable patrons to sponsor screenwriters with gasless, secure onchain wallet transactions. Page: https://arcprompts.lovable.app/ideas/writing-scriptsponsorswap-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptSponsorSwap" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Enable patrons to sponsor screenwriters with gasless, secure onchain wallet transactions. Discipline: Writing, Poetry & Narrative (screenwriter patronage). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy powered wallet with sponsored tx ensures seamless, no-cost patronage flows. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptSponsorSwap.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogScriptSponsorSwap /// @notice Enable patrons to sponsor screenwriters with gasless, secure onchain wallet transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogScriptSponsorSwap { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptSponsorSwap` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptSponsorSwap.sol ScriptSponsorSwap` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every screenwriter patronage action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-scriptstand-18 · writing · ScriptStand ===== Pitch: Buy and sell scripts with frictionless, gasless onchain payments linked to social wallets. Page: https://arcprompts.lovable.app/ideas/writing-scriptstand-18 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptStand" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Buy and sell scripts with frictionless, gasless onchain payments linked to social wallets. Discipline: Writing, Poetry & Narrative (script marketplace). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy embedded wallet with sponsored tx simplifies script transactions without cost barriers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptStand.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogScriptStand /// @notice Buy and sell scripts with frictionless, gasless onchain payments linked to social wallets. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogScriptStand { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptStand` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptStand.sol ScriptStand` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every script marketplace action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-scriptsync-12 · writing · ScriptSync ===== Pitch: Mint collaborative screenplay segments as NFTs to prove multi-author ownership securely. Page: https://arcprompts.lovable.app/ideas/writing-scriptsync-12 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptSync" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint collaborative screenplay segments as NFTs to prove multi-author ownership securely. Discipline: Writing, Poetry & Narrative (collaborative scripts). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance tracks multiple creators with immutable on-chain proofs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptSync.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title ScriptSync /// @notice ERC-721 provenance for: Mint collaborative screenplay segments as NFTs to prove multi-author ownership securely. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ScriptSync is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("ScriptSync", "SCRIPT") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptSync` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptSync.sol ScriptSync` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a collaborative scripts, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-scriptsync-7 · writing · ScriptSync ===== Pitch: Version screenplays with transparent, sponsored blockchain transactions for seamless updates. Page: https://arcprompts.lovable.app/ideas/writing-scriptsync-7 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptSync" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Version screenplays with transparent, sponsored blockchain transactions for seamless updates. Discipline: Writing, Poetry & Narrative (screenplay versioning). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored transactions remove friction in multi-version screenplay updates onchain. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptSync.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogScriptSync /// @notice Version screenplays with transparent, sponsored blockchain transactions for seamless updates. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogScriptSync { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptSync` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptSync.sol ScriptSync` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every screenplay versioning action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-scriptvote-16 · writing · ScriptVote ===== Pitch: Collect and manage anonymous peer feedback on screenplays via secure onchain voting. Page: https://arcprompts.lovable.app/ideas/writing-scriptvote-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "ScriptVote" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Collect and manage anonymous peer feedback on screenplays via secure onchain voting. Discipline: Writing, Poetry & Narrative (script feedback). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts ensure transparent, tamper-proof voting results for constructive critique. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/ScriptVote.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title ScriptVote /// @notice Collect and manage anonymous peer feedback on screenplays via secure onchain voting. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract ScriptVote { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs ScriptVote` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/ScriptVote.sol ScriptVote` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a script feedback action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-sonnet-seal-9 · writing · Sonnet Seal ===== Pitch: Mint sonnets as NFTs to certify original poetic works and preserve ownership rights. Page: https://arcprompts.lovable.app/ideas/writing-sonnet-seal-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Sonnet Seal" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint sonnets as NFTs to certify original poetic works and preserve ownership rights. Discipline: Writing, Poetry & Narrative (classic poetry). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: ERC-721 tokens provide tamper-proof sonnet provenance on Sepolia. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/SonnetSeal.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title SonnetSeal /// @notice ERC-721 provenance for: Mint sonnets as NFTs to certify original poetic works and preserve ownership rights. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SonnetSeal is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("SonnetSeal", "SONNET") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs SonnetSeal` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/SonnetSeal.sol SonnetSeal` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a classic poetry, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-story-genre-taxonomy-23 · writing · Story Genre Taxonomy ===== Pitch: Pin permanent genre taxonomies and story type metadata for consistent classification. Page: https://arcprompts.lovable.app/ideas/writing-story-genre-taxonomy-23 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Story Genre Taxonomy" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin permanent genre taxonomies and story type metadata for consistent classification. Discipline: Writing, Poetry & Narrative (genre classification). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: IPFS supports immutable taxonomies with Pinata ensuring provenance of genre data. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StoryGenreTaxonomy.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentStoryGenreTaxonomy /// @notice Pin permanent genre taxonomies and story type metadata for consistent classification. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentStoryGenreTaxonomy { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StoryGenreTaxonomy` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StoryGenreTaxonomy.sol StoryGenreTaxonomy` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a genre classification, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-storysprint-19 · writing · StorySprint ===== Pitch: Participate in timed story challenges with instant, gasless onchain submissions and social proof. Page: https://arcprompts.lovable.app/ideas/writing-storysprint-19 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StorySprint" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Participate in timed story challenges with instant, gasless onchain submissions and social proof. Discipline: Writing, Poetry & Narrative (timed writing). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored transactions remove entry friction for writers in blockchain-backed contests. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StorySprint.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogStorySprint /// @notice Participate in timed story challenges with instant, gasless onchain submissions and social proof. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogStorySprint { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StorySprint` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StorySprint.sol StorySprint` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every timed writing action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-storystake-1 · writing · StoryStake ===== Pitch: Engage writers in creating branching stories with frictionless onchain ownership of choices. Page: https://arcprompts.lovable.app/ideas/writing-storystake-1 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "StoryStake" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Engage writers in creating branching stories with frictionless onchain ownership of choices. Discipline: Writing, Poetry & Narrative (interactive narrative). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy social plus gasless transactions enables real-time user participation without blockchain friction. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/StoryStake.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogStoryStake /// @notice Engage writers in creating branching stories with frictionless onchain ownership of choices. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogStoryStake { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs StoryStake` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/StoryStake.sol StoryStake` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every interactive narrative action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-verse-vault-0 · writing · Verse Vault ===== Pitch: Securely mint and prove ownership of original poetry on-chain for authentic literary legacy. Page: https://arcprompts.lovable.app/ideas/writing-verse-vault-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Verse Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely mint and prove ownership of original poetry on-chain for authentic literary legacy. Discipline: Writing, Poetry & Narrative (poetry archiving). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance ensures unalterable proof of poem origin and creator identity. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VerseVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title VerseVault /// @notice ERC-721 provenance for: Securely mint and prove ownership of original poetry on-chain for authentic literary legacy. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VerseVault is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("VerseVault", "VERSEV") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VerseVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VerseVault.sol VerseVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a poetry archiving, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-verse-vault-24 · writing · Verse Vault ===== Pitch: Mint spoken word performances as NFTs to certify creator and preserve provenance. Page: https://arcprompts.lovable.app/ideas/writing-verse-vault-24 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Verse Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Mint spoken word performances as NFTs to certify creator and preserve provenance. Discipline: Writing, Poetry & Narrative (spoken word poetry). Onchain primitive: NFT priced in USDC / EURC / cirBTC. Why this primitive: NFT provenance anchors multimedia IPFS content with creator ownership proofs. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VerseVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @title VerseVault /// @notice ERC-721 provenance for: Mint spoken word performances as NFTs to certify creator and preserve provenance. /// @notice Accepts USDC, EURC or cirBTC as payment on Arc Testnet. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VerseVault is ERC721 { uint256 public nextId; mapping(uint256 => string) public cidOf; mapping(uint256 => address) public tokenOf; mapping(uint256 => uint256) public priceOf; address public immutable treasury; constructor(address _treasury) ERC721("VerseVault", "VERSEV") { treasury = _treasury; } /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function mint(address token, uint256 amount, string calldata cid) external returns (uint256 id) { require(IERC20(token).transferFrom(msg.sender, treasury, amount), "pay"); id = ++nextId; cidOf[id] = cid; tokenOf[id] = token; priceOf[id] = amount; _safeMint(msg.sender, id); } function tokenURI(uint256 id) public view override returns (string memory) { return string(abi.encodePacked("data:application/json,{\"cid\":\"", cidOf[id], "\"}")); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VerseVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VerseVault.sol VerseVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. After the user creates a spoken word poetry, they pick USDC / EURC / cirBTC, approve the token, then call `mint(token, amount, cid)` via Privy. Show tokenId, the paid token+amount, and an arcscan link (https://testnet.arcscan.app/tx/). CID can be an on-chain data URI. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-verse-visualizer-3 · writing · Verse Visualizer ===== Pitch: Upload and preserve poetic image metaphors linked to verse for lasting inspiration. Page: https://arcprompts.lovable.app/ideas/writing-verse-visualizer-3 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Verse Visualizer" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Upload and preserve poetic image metaphors linked to verse for lasting inspiration. Discipline: Writing, Poetry & Narrative (poetical imagery). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata enables reliable pinning of paired image and JSON metadata representing poems. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VerseVisualizer.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentVerseVisualizer /// @notice Upload and preserve poetic image metaphors linked to verse for lasting inspiration. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentVerseVisualizer { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VerseVisualizer` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VerseVisualizer.sol VerseVisualizer` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a poetical imagery, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-verseauction-5 · writing · VerseAuction ===== Pitch: Sell original poems as verified digital assets with ownership recorded on Ethereum Sepolia. Page: https://arcprompts.lovable.app/ideas/writing-verseauction-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VerseAuction" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Sell original poems as verified digital assets with ownership recorded on Ethereum Sepolia. Discipline: Writing, Poetry & Narrative (poetry marketplace). Onchain primitive: Arc smart contract. Why this primitive: Smart contracts enable secure, transparent poem sales and ownership transfers. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VerseAuction.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title VerseAuction /// @notice Sell original poems as verified digital assets with ownership recorded on Ethereum Sepolia. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VerseAuction { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VerseAuction` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VerseAuction.sol VerseAuction` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a poetry marketplace action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-verseledger-0 · writing · VerseLedger ===== Pitch: Securely timestamp and prove original poem authorship on the blockchain to prevent plagiarism. Page: https://arcprompts.lovable.app/ideas/writing-verseledger-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VerseLedger" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely timestamp and prove original poem authorship on the blockchain to prevent plagiarism. Discipline: Writing, Poetry & Narrative (poetry archiving). Onchain primitive: Arc smart contract. Why this primitive: Immutable contract records create verifiable proof of original literary creation dates. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VerseLedger.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title VerseLedger /// @notice Securely timestamp and prove original poem authorship on the blockchain to prevent plagiarism. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract VerseLedger { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VerseLedger` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VerseLedger.sol VerseLedger` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User performs a poetry archiving action; the app calls `log(token, amount, payload)` (token = USDC/EURC/cirBTC) via Privy and shows the arcscan link as proof. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-versevault-0 · writing · VerseVault ===== Pitch: Securely draft and store poems with seamless gasless transaction experience. Page: https://arcprompts.lovable.app/ideas/writing-versevault-0 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VerseVault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Securely draft and store poems with seamless gasless transaction experience. Discipline: Writing, Poetry & Narrative (poetry drafting). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored transactions eliminate gas costs, ensuring effortless poem versioning and edits. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VerseVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogVerseVault /// @notice Securely draft and store poems with seamless gasless transaction experience. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogVerseVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VerseVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VerseVault.sol VerseVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every poetry drafting action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-versevaultlive-13 · writing · VerseVaultLive ===== Pitch: Host live poetry writing sessions with instant, gasless collaborative edits and social interaction. Page: https://arcprompts.lovable.app/ideas/writing-versevaultlive-13 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VerseVaultLive" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Host live poetry writing sessions with instant, gasless collaborative edits and social interaction. Discipline: Writing, Poetry & Narrative (live poetry). Onchain primitive: Privy social wallet on Arc. Why this primitive: Gasless sponsored transactions allow uninterrupted, real-time collaborative poetry creation. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VerseVaultLive.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogVerseVaultLive /// @notice Host live poetry writing sessions with instant, gasless collaborative edits and social interaction. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogVerseVaultLive { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VerseVaultLive` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VerseVaultLive.sol VerseVaultLive` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every live poetry action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-versevest-21 · writing · VerseVest ===== Pitch: Manage and distribute poetry royalties transparently via gasless blockchain transactions. Page: https://arcprompts.lovable.app/ideas/writing-versevest-21 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VerseVest" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Manage and distribute poetry royalties transparently via gasless blockchain transactions. Discipline: Writing, Poetry & Narrative (poetry royalties). Onchain primitive: Privy social wallet on Arc. Why this primitive: Privy wallet plus sponsored tx simplifies fair royalty splits without gas burden. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VerseVest.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogVerseVest /// @notice Manage and distribute poetry royalties transparently via gasless blockchain transactions. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogVerseVest { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VerseVest` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VerseVest.sol VerseVest` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every poetry royalties action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-versevibe-17 · writing · VerseVibe ===== Pitch: Share and tip poems in a gasless, social wallet environment to build fan engagement. Page: https://arcprompts.lovable.app/ideas/writing-versevibe-17 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VerseVibe" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Share and tip poems in a gasless, social wallet environment to build fan engagement. Discipline: Writing, Poetry & Narrative (poetry social feed). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored txs with Privy wallet facilitate seamless social interactions and tipping without gas. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VerseVibe.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogVerseVibe /// @notice Share and tip poems in a gasless, social wallet environment to build fan engagement. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogVerseVibe { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VerseVibe` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VerseVibe.sol VerseVibe` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every poetry social feed action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-versevouch-9 · writing · VerseVouch ===== Pitch: Endorse favorite poems effortlessly with gasless transactions linked to your social identity. Page: https://arcprompts.lovable.app/ideas/writing-versevouch-9 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VerseVouch" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Endorse favorite poems effortlessly with gasless transactions linked to your social identity. Discipline: Writing, Poetry & Narrative (poetry endorsement). Onchain primitive: Privy social wallet on Arc. Why this primitive: Sponsored txs tied to Privy wallet enable smooth, cost-free poem endorsements. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VerseVouch.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogVerseVouch /// @notice Endorse favorite poems effortlessly with gasless transactions linked to your social identity. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogVerseVouch { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VerseVouch` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VerseVouch.sol VerseVouch` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every poetry endorsement action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-versevoyage-5 · writing · VerseVoyage ===== Pitch: Host and join gasless onchain poetry contests with social verification and prizes. Page: https://arcprompts.lovable.app/ideas/writing-versevoyage-5 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "VerseVoyage" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Host and join gasless onchain poetry contests with social verification and prizes. Discipline: Writing, Poetry & Narrative (poetry contests). Onchain primitive: Privy social wallet on Arc. Why this primitive: Gasless sponsored txs enable fair competition entry and reward distribution without cost. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/VerseVoyage.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title SocialLogVerseVoyage /// @notice Host and join gasless onchain poetry contests with social verification and prizes. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract SocialLogVerseVoyage { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs VerseVoyage` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/VerseVoyage.sol VerseVoyage` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. Every poetry contests action the user performs is sent as an Arc tx (`log(USDC, 0, payload)`) and displayed with an arcscan link. USDC pays the gas transparently. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-writer-s-journal-vault-16 · writing · Writer’s Journal Vault ===== Pitch: Pin personal writing journals and progress snapshots for secure, permanent archival. Page: https://arcprompts.lovable.app/ideas/writing-writer-s-journal-vault-16 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Writer’s Journal Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Pin personal writing journals and progress snapshots for secure, permanent archival. Discipline: Writing, Poetry & Narrative (personal writing logs). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata ensures private journals are securely pinned on IPFS with privacy controls. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/WritersJournalVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentWritersJournalVault /// @notice Pin personal writing journals and progress snapshots for secure, permanent archival. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentWritersJournalVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs WritersJournalVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/WritersJournalVault.sol WritersJournalVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a personal writing logs, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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 ===== IDEA writing-writer-s-prompt-vault-6 · writing · Writer’s Prompt Vault ===== Pitch: Curate and preserve evolving writing prompts and challenges with immutable timestamps. Page: https://arcprompts.lovable.app/ideas/writing-writer-s-prompt-vault-6 Network setup: [Arc Testnet] ARC_NETWORK=testnet · chainId 5042002 · RPC https://rpc.testnet.arc.network · Explorer https://testnet.arcscan.app · Circle blockchain ARC-TESTNET Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a (6) · cirBTC 0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF (8) Secrets: CIRCLE_API_KEY (TEST key), PRIVY_APP_ID Faucet: https://faucet.circle.com/ (free test USDC/EURC/cirBTC) [Arc Mainnet] ARC_NETWORK=mainnet · chainId 5042 · RPC https://rpc.mainnet.arc.io · Explorer https://explorer.arc.io · Circle blockchain ARC Tokens: USDC 0x3600000000000000000000000000000000000000 (ERC-20 6 dec, native 18) · EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1 (6) · cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0 (8) Secrets: CIRCLE_API_KEY (LIVE key), PRIVY_APP_ID, ARC_NETWORK=mainnet, optional ARC_MAINNET_RPC_URL (server-only) No faucet — fund the treasury with REAL USDC. Real money. Build "Writer’s Prompt Vault" in ONE Lovable message. Single-page demo on Circle's Arc Testnet. CONCEPT Curate and preserve evolving writing prompts and challenges with immutable timestamps. Discipline: Writing, Poetry & Narrative (creative prompts). Onchain primitive: USDC / EURC / cirBTC payments. Why this primitive: Pinata’s IPFS pinning creates permanent prompt archives that can’t be altered or lost. 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, <=100 lines, deployed to Arc Testnet via Circle SCP, verified on arcscan. - Privy is always the auth + wallet layer (Google login, embedded wallet, defaultChain = Arc 5042002). - USDC is the gas token on Arc (no ETH). Support USDC + EURC + cirBTC as payment tokens. - At most ONE AI call per user action (Lovable AI Gateway with LOVABLE_API_KEY). - Skip tests, skip CI, skip docs pages. Ship the demo, nothing else. STACK - React + Vite single page (the index route). - Arc chain definitions (viem `defineChain`, testnet + mainnet, native decimals 18 — USDC is gas): ```ts // src/lib/arc-chain.ts import { defineChain } from "viem"; // USDC is the gas token. NATIVE balance (eth_getBalance) uses 18 decimals, like ETH. // The USDC ERC-20 interface at 0x3600000000000000000000000000000000000000 uses 6. Keep nativeCurrency.decimals = 18, // and use 6 only when reading/sending via the ERC-20 interface. // Testnet RPC: pin `.network` (the `.io` testnet host intermittently DNS-fails). export const arcTestnet = defineChain({ id: 5042002, name: "Arc Testnet", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.testnet.arc.network"] } }, blockExplorers: { default: { name: "Arcscan", url: "https://testnet.arcscan.app" } }, testnet: true, }); // Mainnet: public RPC below; for production use an Alchemy URL stored as the server secret // ARC_MAINNET_RPC_URL (https://arc-mainnet.g.alchemy.com/v2/KEY) and proxy it — never ship it to the browser. export const arcMainnet = defineChain({ id: 5042, name: "Arc", nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 18 }, rpcUrls: { default: { http: ["https://rpc.mainnet.arc.io"] } }, blockExplorers: { default: { name: "Arc Explorer", url: "https://explorer.arc.io" } }, }); export const arcChain = (n: "testnet" | "mainnet") => (n === "mainnet" ? arcMainnet : arcTestnet); ``` - Privy embedded wallet. Wrap in `` + `React.lazy(() => import('./privy-root'))` — Workerd crashes on static import of `@privy-io/react-auth`. The provider config: import { PrivyProvider } from "@privy-io/react-auth"; import { arcTestnet } from "@/lib/arc-chain"; Both `defaultChain` AND `supportedChains` are required; the `as never` cast is required (Privy's chain type is narrow). Read `privyAppId` and `treasuryAddress` from a TanStack route loader (see BOOTSTRAP note below) — NOT from `import.meta.env.VITE_*`. - Multi-stablecoin support is MANDATORY. Ship src/lib/tokens.ts exactly: ```ts // src/lib/tokens.ts - the three stablecoins every Creative Blockchain app must support. // Network is chosen by ARC_NETWORK ("testnet" default | "mainnet"), exposed to the UI via a route loader. // Token amounts: USDC 6, EURC 6, cirBTC 8 (ERC-20 interfaces). Never mix testnet and mainnet addresses. export const NETWORKS = { testnet: { chainId: 5042002, circleBlockchain: "ARC-TESTNET", explorer: "https://testnet.arcscan.app", faucet: "https://faucet.circle.com/", tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a", cirBTC: "0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF" } }, mainnet: { chainId: 5042, circleBlockchain: "ARC", explorer: "https://explorer.arc.io", faucet: null, tokens: { USDC: "0x3600000000000000000000000000000000000000", EURC: "0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1", cirBTC: "0x171A4217b86A807A64eB94757Db6849fb4bDbAA0" } }, } as const; export type ArcNetwork = keyof typeof NETWORKS; export const tokensFor = (n: ArcNetwork) => ({ USDC: { symbol: "USDC", address: NETWORKS[n].tokens.USDC, decimals: 6, label: "US Dollar (gas)" }, EURC: { symbol: "EURC", address: NETWORKS[n].tokens.EURC, decimals: 6, label: "Euro Coin" }, cirBTC: { symbol: "cirBTC", address: NETWORKS[n].tokens.cirBTC, decimals: 8, label: "Circle Wrapped BTC" }, }); ``` - Token switcher UI: three chips (USDC · EURC · cirBTC). The user picks one before paying. GATE tokens whose address is `0x0000…0000` (config missing) — disable the chip and show a hint. Never silently skip the `approve()` step; that looks like a bug. - Pin `solc@0.8.24` in package.json devDependencies (NOT `^0.8.24`). A newer local solc yields bytecode that Arcscan cannot reproduce. - Frontend reads with viem: `createPublicClient({ chain: arcTestnet, transport: http() })`. CONTRACT (contracts/WritersPromptVault.sol): ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @title PaymentWritersPromptVault /// @notice Curate and preserve evolving writing prompts and challenges with immutable timestamps. /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 contract PaymentWritersPromptVault { // token = one of USDC / EURC / cirBTC (ERC-20 addresses configured off-chain) event Logged(address indexed author, address indexed token, uint256 amount, string cid, uint256 at); /// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14 function log(address token, uint256 amount, string calldata cid) external { emit Logged(msg.sender, token, amount, cid, block.timestamp); } } ``` BOOTSTRAP (MANDATORY — runs once in the Lovable sandbox before deploy): - Participant pastes ONLY `CIRCLE_API_KEY` + `PRIVY_APP_ID`. The rest is generated here. - Ship `scripts/bootstrap-circle.mjs` exactly as below and run it once (`node scripts/bootstrap-circle.mjs`). It generates + registers CIRCLE_ENTITY_SECRET, mints the ARC-TESTNET treasury wallet, and prints the three values (secret, wallet ID, address) to paste into Project Settings -> Secrets. - IMPORTANT: name the address secret `CIRCLE_TREASURY_ADDRESS` — NOT `VITE_CIRCLE_TREASURY_ADDRESS`. Lovable's secret store REJECTS any `VITE_*` prefix. Expose it to the client via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS` on the server and returns it as loader data. Same trick for `PRIVY_APP_ID`. - The deploy script MUST refuse to run if `CIRCLE_TREASURY_WALLET_ID` or `CIRCLE_ENTITY_SECRET` is missing, with the exact message: `Run: node scripts/bootstrap-circle.mjs`. ```js // scripts/bootstrap-circle.mjs // MANDATORY first step. Runs once in the Lovable sandbox. Idempotent. // Uses CIRCLE_API_KEY to (1) generate + register CIRCLE_ENTITY_SECRET, // (2) create the ARC-TESTNET developer-controlled treasury wallet, // (3) print all three values for the participant to paste into Project Settings -> Secrets. import crypto from "node:crypto"; import fs from "node:fs"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; if (!KEY) { console.error("Paste CIRCLE_API_KEY first (Project Settings -> Secrets)."); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; // 1. Prefer an explicitly provided entity secret, then a recovery file, then generate new. const RECOVERY = "circle-entity-recovery.json"; let entitySecret = process.env.CIRCLE_ENTITY_SECRET || (fs.existsSync(RECOVERY) ? JSON.parse(fs.readFileSync(RECOVERY,"utf8")).entitySecret : null) || crypto.randomBytes(32).toString("hex"); const provided = !!process.env.CIRCLE_ENTITY_SECRET; // CRITICAL: persist BEFORE any network call. If registration succeeds and the // process dies, the hex is unrecoverable — Circle never reveals it back. // `wx` = fail if the file already exists, so an earlier secret is never clobbered. try { fs.writeFileSync(RECOVERY, JSON.stringify({ entitySecret, savedAt: new Date().toISOString(), warning: "Keep safe. Circle never reveals this again. This hex IS the entity secret.", }, null, 2), { flag: "wx" }); console.log("Entity secret saved to", RECOVERY, "(pre-registration)."); } catch (e) { if (e.code !== "EEXIST") throw e; console.log(RECOVERY, "already exists — reusing it, not overwriting."); } // 2. RSA-OAEP-SHA256 encrypt with Circle's public key and register (POST, not PUT). const { data: { publicKey } } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); const ciphertext = crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const reg = await fetch(`${API}/config/entity/entitySecret`, { method: "POST", headers: H, body: JSON.stringify({ entitySecretCiphertext: ciphertext }) }); if (reg.status !== 200 && reg.status !== 201 && reg.status !== 409) { console.error("register failed", reg.status, await reg.text()); process.exit(1); } if (reg.status === 409 && !provided) { console.error(` 409 = this Circle account ALREADY has an entity secret registered, and Circle never reveals it again. This is EXPECTED even on a brand-new account (onboarding pre-primes the entity). Do NOT create more Circle accounts — they 409 too. Do this instead: RELIABLE FIX — Circle Console configurator: 1. Your 32-byte hex is already saved in ${RECOVERY}. 2. Print the base64 ciphertext for it: node -e "import('node:crypto').then(async c=>{const fs=await import('node:fs');const es=JSON.parse(fs.readFileSync('${RECOVERY}','utf8')).entitySecret;const pk=(await (await fetch('${API}/config/entity/publicKey',{headers:{Authorization:'Bearer '+process.env.CIRCLE_API_KEY}})).json()).data.publicKey;console.log(c.publicEncrypt({key:c.createPublicKey(pk),oaepHash:'sha256',padding:c.constants.RSA_PKCS1_OAEP_PADDING},Buffer.from(es,'hex')).toString('base64'))})" 3. Paste it into Circle Console -> Configurator -> "Entity Secret Ciphertext". 4. Re-run: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs If you already know the registered hex, skip straight to: CIRCLE_ENTITY_SECRET= node scripts/bootstrap-circle.mjs NOTE: the recovery blob Circle hands back on registration is NOT the entity secret. Only the 64-character hex goes into CIRCLE_ENTITY_SECRET. `); process.exit(1); } // 3. Create treasury wallet on ARC-TESTNET. Ciphertext is single-use — regenerate. const freshCipher = () => crypto.publicEncrypt( { key: crypto.createPublicKey(publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(entitySecret, "hex") ).toString("base64"); const setRes = await fetch(`${API}/developer/walletSets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), name: "arc-treasury" }) }).then(r => r.json()); const walletSetId = setRes.data?.walletSet?.id; if (!walletSetId) { console.error("wallet set create failed", JSON.stringify(setRes)); process.exit(1); } const walletRes = await fetch(`${API}/developer/wallets`, { method: "POST", headers: H, body: JSON.stringify({ idempotencyKey: crypto.randomUUID(), entitySecretCiphertext: freshCipher(), walletSetId, blockchains: [process.env.ARC_NETWORK === "mainnet" ? "ARC" : "ARC-TESTNET"], count: 1, accountType: "EOA" }) }).then(r => r.json()); const wallet = walletRes.data?.wallets?.[0]; if (!wallet) { console.error("wallet create failed", JSON.stringify(walletRes)); process.exit(1); } console.log("\n=== PASTE THESE INTO Project Settings -> Secrets ===\n"); console.log("CIRCLE_ENTITY_SECRET =", entitySecret); console.log("CIRCLE_TREASURY_WALLET_ID =", wallet.id); console.log("CIRCLE_TREASURY_ADDRESS =", wallet.address, " // NO VITE_ prefix — expose via route loader"); console.log(`\nThen fund the address at https://faucet.circle.com/ (USDC gas + EURC + cirBTC on Arc Testnet).`); console.log(`Address: ${wallet.address}`); ``` DEPLOY (Circle Smart Contract Platform — USDC gas from the treasury wallet, no MetaMask, no Hardhat): - Install: `bun add solc@0.8.24 -d` - Prereq: bootstrap step above completed AND treasury address funded via https://faucet.circle.com/. - Deploy script: ```js // scripts/deploy-arc.mjs — compile with solc, deploy via Circle SCP (USDC gas). // Prereq: `node scripts/bootstrap-circle.mjs` completed AND treasury address funded. // Usage: `node scripts/deploy-arc.mjs [ctorArg1 ctorArg2 ...]` import fs from "node:fs"; import crypto from "node:crypto"; import solc from "solc"; const API = "https://api.circle.com/v1/w3s"; const KEY = process.env.CIRCLE_API_KEY; const WID = process.env.CIRCLE_TREASURY_WALLET_ID; const ES = process.env.CIRCLE_ENTITY_SECRET; // 32-byte hex if (!KEY) { console.error("Missing CIRCLE_API_KEY"); process.exit(1); } if (!WID || !ES) { console.error("Run: node scripts/bootstrap-circle.mjs"); process.exit(1); } const H = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }; const NET = process.env.ARC_NETWORK === "mainnet" ? "mainnet" : "testnet"; // mainnet needs a LIVE Circle key const CFG = NET === "mainnet" ? { blockchain: "ARC", chainId: 5042, explorer: "https://explorer.arc.io" } : { blockchain: "ARC-TESTNET", chainId: 5042002, explorer: "https://testnet.arcscan.app" }; async function encryptEntitySecret() { const { data } = await fetch(`${API}/config/entity/publicKey`, { headers: H }).then(r => r.json()); return crypto.publicEncrypt( { key: crypto.createPublicKey(data.publicKey), oaepHash: "sha256", padding: crypto.constants.RSA_PKCS1_OAEP_PADDING }, Buffer.from(ES, "hex") ).toString("base64"); // single-use — regen every call } function compile(name) { const source = fs.readFileSync(`contracts/${name}.sol`, "utf8"); const input = { language: "Solidity", sources: { [`${name}.sol`]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } } } }; const out = JSON.parse(solc.compile(JSON.stringify(input))); const c = out.contracts[`${name}.sol`][name]; return { abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }; } const [, , name, ...ctorArgs] = process.argv; const { abi, bytecode } = compile(name); // Circle SCP payload shape that ACTUALLY works (learned the hard way): // - idempotencyKey required // - abiJson is a STRING (JSON.stringify), not an array // - constructorParameters is positional // - feeLevel is a flat string, NOT nested `fee: { type, config }` const body = { idempotencyKey: crypto.randomUUID(), name, walletId: WID, blockchain: CFG.blockchain, abiJson: JSON.stringify(abi), bytecode, constructorParameters: ctorArgs, feeLevel: "MEDIUM", entitySecretCiphertext: await encryptEntitySecret(), }; const deployRes = await fetch(`${API}/contracts/deploy`, { method: "POST", headers: H, body: JSON.stringify(body) }).then(r => r.json()); const contractId = deployRes.data?.contractId; if (!contractId) { console.error("Deploy failed:", JSON.stringify(deployRes, null, 2)); process.exit(1); } // Poll `contract.status === "COMPLETE"` (NOT `state`). let addr; for (let i = 0; i < 60 && !addr; i++) { await new Promise(r => setTimeout(r, 2000)); const { data: { contract } } = await fetch(`${API}/contracts/${contractId}`, { headers: H }).then(r => r.json()); if (contract.status === "COMPLETE") addr = contract.contractAddress; } fs.mkdirSync("src/data", { recursive: true }); fs.writeFileSync("src/data/contract.json", JSON.stringify({ address: addr, abi, network: NET, chainId: CFG.chainId, explorer: CFG.explorer }, null, 2)); console.log(`Deployed ${name} -> ${addr} (${CFG.explorer}/address/${addr})`); ``` - Run: `node scripts/deploy-arc.mjs WritersPromptVault` VERIFY (arcscan / Blockscout standard-JSON — no API key, no Etherscan, no Hardhat, no flattening): ```js // scripts/verify-arc.mjs — Blockscout standard-JSON verify. NO API key. NO plugin. NO flattening. import fs from "node:fs"; import solc from "solc"; const { address: addr, explorer: EXPLORER } = JSON.parse(fs.readFileSync("src/data/contract.json", "utf8")); // EXPLORER = https://testnet.arcscan.app (testnet) or https://explorer.arc.io (mainnet) — both Blockscout. const [, , sourcePath, contractName] = process.argv; // e.g. contracts/Provenance.sol Provenance const source = fs.readFileSync(sourcePath, "utf8"); const fileName = sourcePath.split("/").pop(); // Derive compiler version from the local solc so it EXACTLY matches deploy. // Pin `solc@0.8.24` in devDependencies — newer local solc yields bytecode Arcscan can't reproduce. const compilerversion = "v" + solc.version().split(".Emscripten")[0]; const input = JSON.stringify({ language: "Solidity", sources: { [fileName]: { content: source } }, settings: { optimizer: { enabled: true, runs: 200 }, outputSelection: { "*": { "*": ["abi", "evm.bytecode.object"] } }, }, }); const params = new URLSearchParams({ module: "contract", action: "verifysourcecode", contractaddress: addr, contractname: `${fileName}:${contractName}`, compilerversion, optimizationUsed: "1", runs: "200", sourceCode: input, codeformat: "solidity-standard-json-input", licenseType: "3", constructorArguments: "", autodetectConstructorArguments: "true", }); const res = await fetch(EXPLORER + "/api?" + params.toString(), { method: "POST" }); console.log(res.status, await res.text()); // 200 with "Smart-contract verification started" means QUEUED, not verified. // Poll GET ${EXPLORER}/api/v2/smart-contracts/${addr} until is_verified: true (~1-2 min). ``` - Run: `node scripts/verify-arc.mjs contracts/WritersPromptVault.sol WritersPromptVault` - HTTP 200 with `Smart-contract verification started` = QUEUED, not verified. Poll `GET https://testnet.arcscan.app/api/v2/smart-contracts/` until `is_verified: true` (~1-2 min). - On success, source + ABI are readable at `https://testnet.arcscan.app/address/
?tab=contract`. USER FLOW 1. Land on page -> 'Sign in with Google' (Privy) -> embedded Arc wallet auto-provisioned on chain 5042002. 2. TREASURY CARD (mandatory UI): render a callout that reads the `treasuryAddress` returned by the route loader (server reads `process.env.CIRCLE_TREASURY_ADDRESS`), shows the address with a copy button, and links to https://faucet.circle.com/ with the text "Fund the treasury with USDC (gas) + EURC + cirBTC on Arc Testnet, then reload." If empty, render: "Run `node scripts/bootstrap-circle.mjs` first." 3. User picks USDC / EURC / cirBTC to pay for a creative prompts, approves the ERC-20, then the app calls `log(token, amount, cid)` on the contract via Privy. Render the token symbol, amount, and arcscan tx link. 4. PRE-CONFIRMATION SUMMARY (mandatory, above every approve/tx button): "You'll approve {amount} {symbol} to be spent by {contractName}. Token: 0xABCD…1234." Privy's modal always shows the gas-token (USDC) balance and leaves EURC/cirBTC approval amounts BLANK — that's not a bug. Your summary is what tells the user what they're actually signing. 5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14" REQUIRED SECRETS --- YOU PASTE (2 keys, that's it) --- - CIRCLE_API_KEY Circle Console API key (backend). https://console.circle.com/ - PRIVY_APP_ID Google sign-in + embedded Arc wallet. https://dashboard.privy.io --- OPTIONAL (mainnet only) --- - ARC_NETWORK "testnet" (default) or "mainnet". Build + demo on testnet first. - ARC_MAINNET_RPC_URL Alchemy Arc mainnet RPC (https://arc-mainnet.g.alchemy.com/v2/KEY). Server-only; proxy it. Mainnet also needs a LIVE Circle API key. --- AUTO-GENERATED by `node scripts/bootstrap-circle.mjs` (DO NOT paste manually) --- - CIRCLE_ENTITY_SECRET 32-byte hex, registered with Circle via RSA-OAEP. - CIRCLE_TREASURY_WALLET_ID Developer-controlled wallet on ARC-TESTNET. - CIRCLE_TREASURY_ADDRESS The wallet address. Exposed to the UI via a TanStack route loader that reads `process.env.CIRCLE_TREASURY_ADDRESS`. DO NOT name this `VITE_CIRCLE_TREASURY_ADDRESS` — Lovable's secret store REJECTS any `VITE_*` prefix (reserved for the build). --- OPTIONAL --- - CIRCLE_KIT_KEY Circle App Kit client key for Bridge / Swap widgets. - LOVABLE_API_KEY Only if the idea calls an LLM. Lovable AI Gateway. NO MetaMask key. NO Etherscan key. NO Hardhat. NO Alchemy. Circle pays gas in USDC from the treasury wallet. ENTITY-SECRET RULES (read before running bootstrap — this is the #1 time sink) - Registration is `POST /v1/w3s/config/entity/entitySecret`. NEVER `PUT` — PUT returns 404. - `409 already registered` is EXPECTED, even with a brand-new Circle account and a brand-new API key: Circle's onboarding pre-primes the entity. Do NOT open more Circle accounts to escape it. The reliable path is the Console configurator: keep the 32-byte hex you minted locally, RSA-OAEP-SHA256 encrypt it against `GET /v1/w3s/config/entity/publicKey`, and paste the resulting BASE64 CIPHERTEXT into Circle Console -> Configurator -> "Entity Secret Ciphertext". Then re-run bootstrap with `CIRCLE_ENTITY_SECRET=`. - The recovery blob Circle returns on registration is an encrypted envelope, NOT the secret. Only the 64-character hex goes into `CIRCLE_ENTITY_SECRET`. Pasting the blob yields `invalid entity secret` on every subsequent call. - Persist the hex to `circle-entity-recovery.json` BEFORE any network call, with the `wx` write flag so an existing secret is never silently overwritten. If registration succeeds and you never persisted the hex, the wallet set is unrecoverable — Circle never reveals it back. - ONE entity secret per Circle account. A new API key on the same account inherits the same registered secret; it does not reset the write-once slot. - Entity-secret ciphertext is SINGLE-USE. Re-encrypt for every request (wallet set, wallet, deploy). Reusing a ciphertext is rejected. CIRCLE HARD RULES - NEVER install `@circle-fin/*` SDKs — they assume Node and crash the live (edge) runtime. Hand-roll ~150 lines of `fetch` against https://api.circle.com/v1/w3s using Web Crypto. - Always send `feeLevel: "MEDIUM"`; never a `gasPrice` or EIP-1559 fee object. - Re-encrypt the entity secret on EVERY request (fetch /config/entity/publicKey, RSA-OAEP SHA-256). - Read event history from the Arcscan v2 REST API (`/api/v2/addresses//transactions`); Alchemy's Arc endpoint caps `eth_getLogs` at 10 blocks. Use the public RPC for small windows. - Every server module must boot with zero secrets and return demo data flagged `simulated: true`. NETWORK SWITCH (testnet default, mainnet opt-in) - One `NETWORKS` table: chainId, rpc, explorer, faucet, Circle blockchain id, token addresses. - Server reads `ARC_NETWORK` ("testnet" | "mainnet"); UI keeps the choice in localStorage and shows a Testnet/Mainnet toggle. Key the Privy provider by network (`key={network}`). - If a contract isn't deployed on mainnet, show a "Not deployed on mainnet yet — switch to testnet" card. - Keep any Alchemy mainnet URL server-only (`ARC_MAINNET_RPC_URL`), proxied via a server function. - On mainnet hide the faucet link and show a red "real money" warning above every pay button. - Files over 10 MB never go in the project: use `lovable-assets create` + `.asset.json` pointers. GAS ON ARC (no paymaster needed) USDC IS the native gas token on Arc (testnet and mainnet) — there is no ETH and no sponsorship policy to configure. Do NOT add a Privy paymaster / gas-sponsorship config; it is not needed and will not apply. Fund the treasury AND the user's embedded wallet with USDC from the Circle faucet and Privy signs normally. If a Privy modal ever asks for ETH, the wallet is on the wrong chain or simply unfunded. TROUBLESHOOTING (symptom -> cause -> fix) - Secret named `VITE_PRIVY_APP_ID` / `VITE_CIRCLE_TREASURY_ADDRESS` is rejected -> Lovable reserves the `VITE_*` prefix -> save as `PRIVY_APP_ID` / `CIRCLE_TREASURY_ADDRESS` and expose via a TanStack route loader reading `process.env.*` on the server. - RPC calls fail or DNS-error -> you used `rpc.testnet.arc.io` -> use `https://rpc.testnet.arc.network`. - Circle returns 409 on a brand-new API key -> onboarding pre-primed the entity -> register your own ciphertext through the Console configurator (see ENTITY-SECRET RULES). - `invalid entity secret` after pasting -> you pasted the recovery blob -> paste the 64-hex value. - SCP deploy returns 400 -> missing `idempotencyKey`, or `abiJson` sent as an array, or `feeLevel` nested as an object -> send a UUID, `JSON.stringify(abi)`, and `feeLevel: "MEDIUM"` flat. - Deploy polls forever -> you're reading `data.contract.state` -> wait for `data.contract.status === "COMPLETE"`, then read `contract.contractAddress`. - Arcscan says "Fail - Unable to verify" -> local solc is newer than the deployed pragma -> pin `solc@0.8.24` in devDependencies (no caret) and use exact `pragma solidity 0.8.24;`, with `optimizer: { enabled: true, runs: 200 }` on BOTH deploy and verify. - Native balance off by 10^12 -> you mixed the two USDC interfaces -> native (eth_getBalance) is 18 decimals (`nativeCurrency.decimals: 18`); the ERC-20 at 0x3600... is 6. - Wallet/tx lands on the wrong network -> chain ID mismatch -> testnet is 5042002, mainnet is 5042. Put BOTH in Privy `supportedChains`; pick `defaultChain` from ARC_NETWORK. - Circle returns 401/400 on mainnet -> testnet API keys only work on ARC-TESTNET -> create a LIVE key in Circle Console and use `blockchain: "ARC"`. Entity secret + wallet are per environment. - Mainnet token transfer reverts / balance 0 -> testnet EURC/cirBTC address used on mainnet -> use `tokensFor(network)`; mainnet EURC 0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1, cirBTC 0x171A4217b86A807A64eB94757Db6849fb4bDbAA0. - Privy modal asks for ETH -> Arc gas is USDC -> fund the wallet at https://faucet.circle.com/. - Privy modal shows a USDC balance while approving EURC/cirBTC, and the approval amount renders BLANK -> Privy always shows the gas-token balance and has no metadata for EURC/cirBTC on Arc -> NOT a bug; render your own pre-confirmation summary (see USER FLOW step 4). - A token's `approve()` step appears to be skipped -> its configured address is `0x0000...0000` -> gate that chip (disabled + hint) instead of skipping approve. - Live site shows `[unenv] ... not implemented` / `__dirname is not defined` / blank 500 -> an `@circle-fin/*` SDK was installed -> remove it; call https://api.circle.com/v1/w3s with fetch. - Circle 400 on a transfer/contract call -> a `gasPrice`/fee object was sent -> use `feeLevel: "MEDIUM"`. - Circle rejects the entitySecretCiphertext -> ciphertext was reused -> re-encrypt on EVERY request. - `eth_getLogs` errors on Alchemy -> 10-block cap on Arc -> read history from Arcscan v2 REST. - Commit fails "exceeds 10 MB" -> a large file was written into the project -> upload it with `lovable-assets create` and commit only the `.asset.json` pointer. DEFINITION OF DONE (verify each one yourself) 1. `node scripts/bootstrap-circle.mjs` prints the entity secret, treasury wallet ID and address, and `circle-entity-recovery.json` exists on disk BEFORE any network call was made. 2. The treasury address is funded at https://faucet.circle.com/ -> "Arc Testnet" (USDC for gas; add EURC and cirBTC if the demo settles in them). 3. `node scripts/deploy-arc.mjs ` polls to `status: "COMPLETE"` and writes `src/data/contract.json` with `address`, `abi` and `chainId: 5042002`. 4. `node scripts/verify-arc.mjs ...` returns HTTP 200 `Smart-contract verification started`, and ~1-2 min later `GET https://testnet.arcscan.app/api/v2/smart-contracts/` reports `is_verified: true` (Arcscan UI shows the green checkmark). 5. Google login via Privy provisions an embedded wallet on chain 5042002, and an approve + contract call confirms on Arcscan under that wallet's address. 6. (Optional, mainnet) With ARC_NETWORK=mainnet, a LIVE Circle key and a treasury funded with REAL USDC (no faucet), the same deploy + verify succeeds on https://explorer.arc.io (chain 5042). Always finish on testnet first. 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